This commit is contained in:
Edouard Oger 2020-01-22 16:17:38 -05:00
Родитель 72bbb67ee0
Коммит 81d45d3df7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: A2F740742307674A
24 изменённых файлов: 2185 добавлений и 274 удалений

Просмотреть файл

@ -3,3 +3,15 @@
# Unreleased Changes
[Full Changelog](https://github.com/mozilla/application-services/compare/v0.48.3...master)
## FxA Client
### What's New
- `FirefoxAccount` is now deprecated
- Introducing `FxAccountManager` which provides a higher-level interface to Firefox Accounts. Among other things, this class handles (and can recover from) authentication errors, exposes device-related account methods, handles its own keychain storage and fires observer notifications for important account events.
### Breaking changes
- `FirefoxAccount.fromJSON(json: String)` has been replaced by the `FirefoxAccount(fromJsonState: String)` constructor.

Просмотреть файл

@ -1 +1,2 @@
github "apple/swift-protobuf" ~> 1.0
github "jrendel/SwiftKeychainWrapper" ~> 3.2.0

Просмотреть файл

@ -1 +1,2 @@
github "apple/swift-protobuf" "1.5.0"
github "apple/swift-protobuf" "1.7.0"
github "jrendel/SwiftKeychainWrapper" "3.4.0"

Просмотреть файл

@ -6,6 +6,7 @@ the details of which are reproduced below.
* [Mozilla Public License 2.0](#mozilla-public-license-20)
* [Apache License 2.0](#apache-license-20)
* [MIT License: SwiftKeychainWrapper](#mit-license-swiftkeychainwrapper)
* [MIT License: aho-corasick, byteorder, memchr, termcolor](#mit-license-aho-corasick-byteorder-memchr-termcolor)
* [MIT License: ansi_term](#mit-license-ansi_term)
* [MIT License: atty](#mit-license-atty)
@ -751,6 +752,37 @@ 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.
```
-------------
## MIT License: SwiftKeychainWrapper
The following text applies to code linked from these dependencies:
[SwiftKeychainWrapper](https://github.com/jrendel/SwiftKeychainWrapper)
```
The MIT License (MIT)
Copyright (c) 2014 Jason Rendel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
-------------
## MIT License: aho-corasick, byteorder, memchr, termcolor

Просмотреть файл

@ -10,6 +10,11 @@ public enum FirefoxAccountError: LocalizedError {
case unspecified(message: String)
case panic(message: String)
// Trying to finish an authentication that was never started with begin(...)Flow.
case noExistingAuthFlow
// Trying to finish a different authentication flow.
case wrongAuthFlow
/// Our implementation of the localizedError protocol -- (This shows up in Sentry)
public var errorDescription: String? {
switch self {
@ -21,6 +26,10 @@ public enum FirefoxAccountError: LocalizedError {
return "FirefoxAccountError.unspecified: \(message)"
case let .panic(message):
return "FirefoxAccountError.panic: \(message)"
case .noExistingAuthFlow:
return "FirefoxAccountError.noExistingAuthFlow"
case .wrongAuthFlow:
return "FirefoxAccountError.wrongAuthFlow"
}
}

Просмотреть файл

@ -3,196 +3,26 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import os.log
import UIKit
open class FxAConfig {
// FIXME: these should be lower case.
// swiftlint:disable identifier_name
public enum Server: String {
case Release = "https://accounts.firefox.com"
case Stable = "https://stable.dev.lcip.org"
case Dev = "https://accounts.stage.mozaws.net"
}
// swiftlint:enable identifier_name
let contentUrl: String
let clientId: String
let redirectUri: String
public init(contentUrl: String, clientId: String, redirectUri: String) {
self.contentUrl = contentUrl
self.clientId = clientId
self.redirectUri = redirectUri
}
public init(withServer server: Server, clientId: String, redirectUri: String) {
contentUrl = server.rawValue
self.clientId = clientId
self.redirectUri = redirectUri
}
public static func release(clientId: String, redirectUri: String) -> FxAConfig {
return FxAConfig(withServer: FxAConfig.Server.Release, clientId: clientId, redirectUri: redirectUri)
}
public static func stable(clientId: String, redirectUri: String) -> FxAConfig {
return FxAConfig(withServer: FxAConfig.Server.Stable, clientId: clientId, redirectUri: redirectUri)
}
public static func dev(clientId: String, redirectUri: String) -> FxAConfig {
return FxAConfig(withServer: FxAConfig.Server.Dev, clientId: clientId, redirectUri: redirectUri)
}
}
public protocol PersistCallback {
func persist(json: String)
}
private let queue = DispatchQueue(label: "com.mozilla.firefox-account")
open class FirefoxAccount {
private let raw: UInt64
private var persistCallback: PersistCallback?
private init(raw: UInt64) {
self.raw = raw
}
/// Create a `FirefoxAccount` from scratch. This is suitable for callers using the
/// OAuth Flow.
/// Please note that the `FxAConfig` provided will be consumed and therefore
/// should not be re-used.
public convenience init(config: FxAConfig) throws {
let pointer = try queue.sync {
try FirefoxAccountError.unwrap { err in
fxa_new(config.contentUrl, config.clientId, config.redirectUri, err)
}
}
self.init(raw: pointer)
}
/// Restore a previous instance of `FirefoxAccount` from a serialized state (obtained with `toJSON(...)`).
open class func fromJSON(state: String) throws -> FirefoxAccount {
return try queue.sync {
let handle = try FirefoxAccountError.unwrap { err in fxa_from_json(state, err) }
return FirefoxAccount(raw: handle)
}
}
deinit {
if self.raw != 0 {
queue.sync {
try! FirefoxAccountError.unwrap { err in
// Is `try!` the right thing to do? We should only hit an error here
// for panics and handle misuse, both inidicate bugs in our code
// (the first in the rust code, the 2nd in this swift wrapper).
fxa_free(self.raw, err)
}
}
}
}
/// Serializes the state of a `FirefoxAccount` instance. It can be restored
/// later with `fromJSON(...)`. It is the responsability of the caller to
/// persist that serialized state regularly (after operations that mutate
/// `FirefoxAccount`) in a **secure** location.
open func toJSON() throws -> String {
return try queue.sync {
try self.toJSONInternal()
}
}
private func toJSONInternal() throws -> String {
return String(freeingFxaString: try FirefoxAccountError.unwrap { err in
fxa_to_json(self.raw, err)
})
}
/// Registers a persistance callback. The callback will get called every time
/// the `FirefoxAccount` state needs to be saved. The callback must
/// persist the passed string in a secure location (like the keychain).
public func registerPersistCallback(_ cb: PersistCallback) {
persistCallback = cb
}
/// Unregisters a persistance callback.
public func unregisterPersistCallback() {
persistCallback = nil
}
private func tryPersistState() {
queue.async {
guard let cb = self.persistCallback else {
return
}
do {
let json = try self.toJSONInternal()
DispatchQueue.global(qos: .background).async {
cb.persist(json: json)
}
} catch {
// Ignore the error because the prior operation might have worked,
// but still log it.
os_log("FirefoxAccount internal state serialization failed.")
}
}
}
/// This class provides low-level access to `RustFxAccount` through various asynchronous wrappers.
/// It should not be used anymore and is kept for backwards compatbility for the Lockwise iOS project.
@available(*, deprecated, message: "Use FxaAccountManager instead")
open class FirefoxAccount: RustFxAccount {
/// Gets the logged-in user profile.
/// Throws `FirefoxAccountError.Unauthorized` if we couldn't find any suitable access token
/// to make that call. The caller should then start the OAuth Flow again with
/// the "profile" scope.
open func getProfile(completionHandler: @escaping (Profile?, Error?) -> Void) {
queue.async {
DispatchQueue.global().async {
do {
let profileBuffer = try FirefoxAccountError.unwrap { err in
fxa_profile(self.raw, false, err)
}
let msg = try! MsgTypes_Profile(serializedData: Data(rustBuffer: profileBuffer))
fxa_bytebuffer_free(profileBuffer)
let profile = Profile(msg: msg)
let profile = try super.getProfile()
DispatchQueue.main.async { completionHandler(profile, nil) }
self.tryPersistState()
} catch {
DispatchQueue.main.async { completionHandler(nil, error) }
}
}
}
open func getTokenServerEndpointURL() throws -> URL {
return try queue.sync {
URL(string: String(freeingFxaString: try FirefoxAccountError.unwrap { err in
fxa_get_token_server_endpoint_url(self.raw, err)
}))!
}
}
open func getConnectionSuccessURL() throws -> URL {
return try queue.sync {
URL(string: String(freeingFxaString: try FirefoxAccountError.unwrap { err in
fxa_get_connection_success_url(self.raw, err)
}))!
}
}
open func getManageAccountURL(entrypoint: String) throws -> URL {
return try queue.sync {
URL(string: String(freeingFxaString: try FirefoxAccountError.unwrap { err in
fxa_get_manage_account_url(self.raw, entrypoint, err)
}))!
}
}
open func getManageDevicesURL(entrypoint: String) throws -> URL {
return try queue.sync {
URL(string: String(freeingFxaString: try FirefoxAccountError.unwrap { err in
fxa_get_manage_devices_url(self.raw, entrypoint, err)
}))!
}
}
/// Request a OAuth token by starting a new OAuth flow.
///
/// This function returns a URL string that the caller should open in a webview.
@ -201,12 +31,9 @@ open class FirefoxAccount {
/// the caller must intercept that redirection, extract the `code` and `state` query parameters and call
/// `completeOAuthFlow(...)` to complete the flow.
open func beginOAuthFlow(scopes: [String], completionHandler: @escaping (URL?, Error?) -> Void) {
queue.async {
DispatchQueue.global().async {
do {
let scope = scopes.joined(separator: " ")
let url = URL(string: String(freeingFxaString: try FirefoxAccountError.unwrap { err in
fxa_begin_oauth_flow(self.raw, scope, err)
}))!
let url = try super.beginOAuthFlow(scopes: scopes)
DispatchQueue.main.async { completionHandler(url, nil) }
} catch {
DispatchQueue.main.async { completionHandler(nil, error) }
@ -219,13 +46,10 @@ open class FirefoxAccount {
/// This resulting token might not have all the `scopes` the caller have requested (e.g. the user
/// might have denied some of them): it is the responsibility of the caller to accomodate that.
open func completeOAuthFlow(code: String, state: String, completionHandler: @escaping (Void, Error?) -> Void) {
queue.async {
DispatchQueue.global().async {
do {
try FirefoxAccountError.unwrap { err in
fxa_complete_oauth_flow(self.raw, code, state, err)
}
try super.completeOAuthFlow(code: code, state: state)
DispatchQueue.main.async { completionHandler((), nil) }
self.tryPersistState()
} catch {
DispatchQueue.main.async { completionHandler((), error) }
}
@ -238,15 +62,9 @@ open class FirefoxAccount {
/// for this scope. The caller should then start the OAuth Flow again with
/// the desired scope.
open func getAccessToken(scope: String, completionHandler: @escaping (AccessTokenInfo?, Error?) -> Void) {
queue.async {
DispatchQueue.global().async {
do {
let infoBuffer = try FirefoxAccountError.unwrap { err in
fxa_get_access_token(self.raw, scope, err)
}
self.tryPersistState()
let msg = try! MsgTypes_AccessTokenInfo(serializedData: Data(rustBuffer: infoBuffer))
fxa_bytebuffer_free(infoBuffer)
let tokenInfo = AccessTokenInfo(msg: msg)
let tokenInfo = try super.getAccessToken(scope: scope)
DispatchQueue.main.async { completionHandler(tokenInfo, nil) }
} catch {
DispatchQueue.main.async { completionHandler(nil, error) }
@ -256,14 +74,9 @@ open class FirefoxAccount {
/// Check whether the refreshToken is active
open func checkAuthorizationStatus(completionHandler: @escaping (IntrospectInfo?, Error?) -> Void) {
queue.async {
DispatchQueue.global().async {
do {
let infoBuffer = try FirefoxAccountError.unwrap { err in
fxa_check_authorization_status(self.raw, err)
}
let msg = try! MsgTypes_IntrospectInfo(serializedData: Data(rustBuffer: infoBuffer))
fxa_bytebuffer_free(infoBuffer)
let tokenInfo = IntrospectInfo(msg: msg)
let tokenInfo = try super.checkAuthorizationStatus()
DispatchQueue.main.async { completionHandler(tokenInfo, nil) }
} catch {
DispatchQueue.main.async { completionHandler(nil, error) }
@ -277,11 +90,9 @@ open class FirefoxAccount {
/// so the caller can try to call `getAccessToken` or `getProfile`
/// again.
open func clearAccessTokenCache(completionHandler: @escaping (Void, Error?) -> Void) {
queue.async {
DispatchQueue.global().async {
do {
try FirefoxAccountError.unwrap { err in
fxa_clear_access_token_cache(self.raw, err)
}
try super.clearAccessTokenCache()
DispatchQueue.main.async { completionHandler((), nil) }
} catch {
DispatchQueue.main.async { completionHandler((), error) }
@ -292,79 +103,13 @@ open class FirefoxAccount {
/// Disconnect from the account and optionaly destroy our device record.
/// `beginOAuthFlow(...)` will need to be called to reconnect.
open func disconnect(completionHandler: @escaping (Void, Error?) -> Void) {
queue.async {
DispatchQueue.global().async {
do {
try FirefoxAccountError.unwrap { err in
fxa_disconnect(self.raw, err)
}
try super.disconnect()
DispatchQueue.main.async { completionHandler((), nil) }
self.tryPersistState()
} catch {
DispatchQueue.main.async { completionHandler((), error) }
}
}
}
}
public struct ScopedKey {
public let kty: String
public let scope: String
public let k: String
public let kid: String
internal init(msg: MsgTypes_ScopedKey) {
kty = msg.kty
scope = msg.scope
k = msg.k
kid = msg.kid
}
}
public struct AccessTokenInfo {
public let scope: String
public let token: String
public let key: ScopedKey?
public let expiresAt: Date
internal init(msg: MsgTypes_AccessTokenInfo) {
scope = msg.scope
token = msg.token
key = msg.hasKey ? ScopedKey(msg: msg.key) : nil
expiresAt = Date(timeIntervalSince1970: Double(msg.expiresAt))
}
}
public struct IntrospectInfo {
public let active: Bool
public let tokenType: String
public let scope: String?
public let exp: Date
public let iss: String?
internal init(msg: MsgTypes_IntrospectInfo) {
active = msg.active
tokenType = msg.tokenType
scope = msg.hasScope ? msg.scope : nil
exp = Date(timeIntervalSince1970: Double(msg.exp))
iss = msg.hasIss ? msg.iss : nil
}
}
public struct Avatar {
public let url: String
public let isDefault: Bool
}
public struct Profile {
public let uid: String
public let email: String
public let avatar: Avatar?
public let displayName: String?
internal init(msg: MsgTypes_Profile) {
uid = msg.uid
email = msg.email
avatar = msg.hasAvatar ? Avatar(url: msg.avatar, isDefault: msg.avatarDefault) : nil
displayName = msg.hasDisplayName ? msg.displayName : nil
}
}

Просмотреть файл

@ -0,0 +1,168 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
/// This class inherits from `RustFxAccount` and adds:
/// - Automatic state persistence through `PersistCallback`.
/// - Auth error signaling through observer notifications.
class FxAccount: RustFxAccount {
private var persistCallback: PersistCallback?
/// Registers a persistance callback. The callback will get called every time
/// the `FxAccounts` state needs to be saved. The callback must
/// persist the passed string in a secure location (like the keychain).
public func registerPersistCallback(_ cb: PersistCallback) {
persistCallback = cb
}
/// Unregisters a persistance callback.
public func unregisterPersistCallback() {
persistCallback = nil
}
override func getProfile() throws -> Profile {
defer { tryPersistState() }
return try notifyAuthErrors {
try super.getProfile()
}
}
override func beginOAuthFlow(scopes: [String]) throws -> URL {
return try notifyAuthErrors {
try super.beginOAuthFlow(scopes: scopes)
}
}
override func beginPairingFlow(pairingUrl: String, scopes: [String]) throws -> URL {
return try notifyAuthErrors {
try super.beginPairingFlow(pairingUrl: pairingUrl, scopes: scopes)
}
}
override func completeOAuthFlow(code: String, state: String) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try super.completeOAuthFlow(code: code, state: state)
}
}
override func getAccessToken(scope: String) throws -> AccessTokenInfo {
defer { tryPersistState() }
return try notifyAuthErrors {
try super.getAccessToken(scope: scope)
}
}
override func disconnect() throws {
defer { tryPersistState() }
try super.disconnect()
}
override func pollDeviceCommands() throws -> [DeviceEvent] {
defer { tryPersistState() }
return try notifyAuthErrors {
try super.pollDeviceCommands()
}
}
override func fetchDevices() throws -> [Device] {
return try notifyAuthErrors {
try super.fetchDevices()
}
}
override func setDevicePushSubscription(endpoint: String, publicKey: String, authKey: String) throws {
try notifyAuthErrors {
try super.setDevicePushSubscription(endpoint: endpoint, publicKey: publicKey, authKey: authKey)
}
}
override func setDeviceDisplayName(_ name: String) throws {
try notifyAuthErrors {
try super.setDeviceDisplayName(name)
}
}
override func handlePushMessage(payload: String) throws -> [DeviceEvent] {
defer { tryPersistState() }
return try notifyAuthErrors {
try super.handlePushMessage(payload: payload)
}
}
override func sendSingleTab(targetId: String, title: String, url: String) throws {
return try notifyAuthErrors {
try super.sendSingleTab(targetId: targetId, title: title, url: url)
}
}
override func initializeDevice(
name: String,
deviceType: DeviceType,
supportedCapabilities: [DeviceCapability]
) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try super.initializeDevice(name: name, deviceType: deviceType, supportedCapabilities: supportedCapabilities)
}
}
override func ensureCapabilities(supportedCapabilities: [DeviceCapability]) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try super.ensureCapabilities(supportedCapabilities: supportedCapabilities)
}
}
override func checkAuthorizationStatus() throws -> IntrospectInfo {
return try notifyAuthErrors {
try self.checkAuthorizationStatus()
}
}
override func clearAccessTokenCache() throws {
try notifyAuthErrors {
try self.clearAccessTokenCache()
}
}
private func tryPersistState() {
DispatchQueue.global().async {
guard let cb = self.persistCallback else {
return
}
do {
let json = try self.toJSON()
DispatchQueue.global(qos: .background).async {
cb.persist(json: json)
}
} catch {
// Ignore the error because the prior operation might have worked,
// but still log it.
FxALog.error("FxAccounts internal state serialization failed.")
}
}
}
internal func notifyAuthErrors<T>(_ cb: () throws -> T) rethrows -> T {
do {
return try cb()
} catch let error as FirefoxAccountError {
if case let .unauthorized(msg) = error {
FxALog.debug("Auth error caught: \(msg)")
notifyAuthError()
}
throw error
}
}
internal func notifyAuthError() {
NotificationCenter.default.post(name: .accountAuthException, object: nil)
}
}
public protocol PersistCallback {
func persist(json: String)
}

Просмотреть файл

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
open class FxAConfig {
// FIXME: these should be lower case.
// swiftlint:disable identifier_name
public enum Server: String {
case Release = "https://accounts.firefox.com"
case Stable = "https://stable.dev.lcip.org"
case Dev = "https://accounts.stage.mozaws.net"
}
// swiftlint:enable identifier_name
let contentUrl: String
let clientId: String
let redirectUri: String
public init(contentUrl: String, clientId: String, redirectUri: String) {
self.contentUrl = contentUrl
self.clientId = clientId
self.redirectUri = redirectUri
}
public init(withServer server: Server, clientId: String, redirectUri: String) {
contentUrl = server.rawValue
self.clientId = clientId
self.redirectUri = redirectUri
}
public static func release(clientId: String, redirectUri: String) -> FxAConfig {
return FxAConfig(withServer: FxAConfig.Server.Release, clientId: clientId, redirectUri: redirectUri)
}
public static func stable(clientId: String, redirectUri: String) -> FxAConfig {
return FxAConfig(withServer: FxAConfig.Server.Stable, clientId: clientId, redirectUri: redirectUri)
}
public static func dev(clientId: String, redirectUri: String) -> FxAConfig {
return FxAConfig(withServer: FxAConfig.Server.Dev, clientId: clientId, redirectUri: redirectUri)
}
}

Просмотреть файл

@ -0,0 +1,129 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
public struct Device {
public let id: String
public let displayName: String
public let deviceType: DeviceType
public let isCurrentDevice: Bool
public let lastAccessTime: UInt64?
public let capabilities: [DeviceCapability]
public let subscriptionExpired: Bool
public let subscription: DevicePushSubscription?
internal static func fromCollectionMsg(msg: MsgTypes_Devices) -> [Device] {
msg.devices.map { Device(msg: $0) }
}
internal init(msg: MsgTypes_Device) {
id = msg.id
displayName = msg.displayName
deviceType = DeviceType.fromMsg(msg: msg.type)
isCurrentDevice = msg.isCurrentDevice
lastAccessTime = msg.hasLastAccessTime ? msg.lastAccessTime : nil
capabilities = msg.capabilities.map { DeviceCapability.fromMsg(msg: $0) }
subscriptionExpired = msg.pushEndpointExpired
subscription = msg.hasPushSubscription ?
DevicePushSubscription(msg: msg.pushSubscription) :
nil
}
}
public enum DeviceType {
case desktop
case mobile
case tablet
case tv
case vr
case unknown
internal static func fromMsg(msg: MsgTypes_Device.TypeEnum) -> DeviceType {
switch msg {
case .desktop: return .desktop
case .mobile: return .mobile
case .tablet: return .tablet
case .tv: return .tv
case .vr: return .vr
case .unknown: return .unknown
}
}
internal func toMsg() -> MsgTypes_Device.TypeEnum {
switch self {
case .desktop: return .desktop
case .mobile: return .mobile
case .tablet: return .tablet
case .tv: return .tv
case .vr: return .vr
case .unknown: return .unknown
}
}
}
public enum DeviceCapability {
case sendTab
internal static func fromMsg(msg: MsgTypes_Device.Capability) -> DeviceCapability {
switch msg {
case .sendTab: return .sendTab
}
}
internal func toMsg() -> MsgTypes_Device.Capability {
switch self {
case .sendTab: return .sendTab
}
}
}
extension Array where Element == DeviceCapability {
internal func toCollectionMsg() -> MsgTypes_Capabilities {
MsgTypes_Capabilities.with {
$0.capability = self.map { $0.toMsg() }
}
}
}
public struct DevicePushSubscription {
public let endpoint: String
public let publicKey: String
public let authKey: String
public init(endpoint: String, publicKey: String, authKey: String) {
self.endpoint = endpoint
self.publicKey = publicKey
self.authKey = authKey
}
internal init(msg: MsgTypes_Device.PushSubscription) {
endpoint = msg.endpoint
publicKey = msg.publicKey
authKey = msg.authKey
}
}
public enum DeviceEvent {
case tabReceived(Device?, [TabData])
internal static func fromCollectionMsg(msg: MsgTypes_AccountEvents) -> [DeviceEvent] {
msg.events.map { DeviceEvent.fromMsg(msg: $0) }
}
internal static func fromMsg(msg: MsgTypes_AccountEvent) -> DeviceEvent {
switch msg.type {
case .tabReceived: do {
let device = msg.tabReceivedData.hasFrom ? Device(msg: msg.tabReceivedData.from) : nil
let entries = msg.tabReceivedData.entries.map { TabData(title: $0.title, url: $0.url) }
return .tabReceived(device, entries)
}
}
}
}
public struct TabData {
public let title: String
public let url: String
}

Просмотреть файл

@ -0,0 +1,158 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
public extension Notification.Name {
static let constellationStateUpdate = Notification.Name("constellationStateUpdate")
}
public struct ConstellationState {
public let localDevice: Device?
public let remoteDevices: [Device]
}
public class DeviceConstellation {
var constellationState: ConstellationState?
let account: FxAccount
init(account: FxAccount) {
self.account = account
}
/// Get local + remote devices synchronously.
/// Note that this state might be empty, which should handle by calling `refreshState()`
/// A `.constellationStateUpdate` notification is fired if the device list changes at any time.
public func state() -> ConstellationState? {
return constellationState
}
/// Refresh the list of remote devices.
/// A `.constellationStateUpdate` notification might get fired once the new device list is fetched.
public func refreshState() {
DispatchQueue.global().async {
FxALog.info("Refreshing device list...")
do {
let devices = try self.account.fetchDevices()
let localDevice = devices.first { $0.isCurrentDevice }
if localDevice?.subscriptionExpired ?? false {
FxALog.debug("Current device needs push endpoint registration.")
}
let remoteDevices = devices.filter { !$0.isCurrentDevice }
let newState = ConstellationState(localDevice: localDevice, remoteDevices: remoteDevices)
self.constellationState = newState
FxALog.debug("Refreshed device list; saw \(devices.count) device(s).")
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .constellationStateUpdate,
object: nil,
userInfo: ["newState": newState]
)
}
} catch {
FxALog.error("Failure fetching the device list: \(error).")
return
}
}
}
/// Updates the local device name.
public func setLocalDeviceName(name: String) {
DispatchQueue.global().async {
do {
try self.account.setDeviceDisplayName(name)
// Update our list of devices in the background to reflect the change.
self.refreshState()
} catch {
FxALog.error("Failure changing the local device name: \(error).")
}
}
}
/// Poll for device events we might have missed (e.g. Push notification missed, or device offline).
/// Your app should probably call this on a regular basic (e.g. once a day).
public func pollForEvents(completionHandler: @escaping (Result<[DeviceEvent], Error>) -> Void) {
DispatchQueue.global().async {
do {
let events = try self.account.pollDeviceCommands()
DispatchQueue.main.async { completionHandler(.success(events)) }
} catch {
DispatchQueue.main.async { completionHandler(.failure(error)) }
}
}
}
/// Send an event to another device such as Send Tab.
public func sendEventToDevice(targetDeviceId: String, e: DeviceEventOutgoing) {
DispatchQueue.global().async {
do {
switch e {
case let .sendTab(title, url): do {
try self.account.sendSingleTab(targetId: targetDeviceId, title: title, url: url)
}
}
} catch {
FxALog.error("Error sending event to another device: \(error).")
}
}
}
/// Register the local AutoPush subscription with the FxA server.
public func setDevicePushSubscription(sub: DevicePushSubscription) {
DispatchQueue.global().async {
do {
try self.account.setDevicePushSubscription(
endpoint: sub.endpoint,
publicKey: sub.publicKey,
authKey: sub.authKey
)
} catch {
FxALog.error("Failure setting push subscription: \(error).")
}
}
}
/// Once Push has decrypted a payload, send the payload to this method
/// which will tell the app what to do with it in form of `DeviceEvents`.
public func processRawIncomingDeviceEvent(pushPayload: String,
completionHandler: @escaping (Result<[DeviceEvent], Error>) -> Void) {
DispatchQueue.global().async {
do {
let events = try self.account.handlePushMessage(payload: pushPayload)
DispatchQueue.main.async { completionHandler(.success(events)) }
} catch {
DispatchQueue.main.async { completionHandler(.failure(error)) }
}
}
}
internal func initDevice(name: String, type: DeviceType, capabilities: [DeviceCapability]) {
// This method is called by `FxAccountManager` on its own asynchronous queue, hence
// no wrapping in a `DispatchQueue.global().async`.
assert(!Thread.isMainThread)
do {
try account.initializeDevice(name: name, deviceType: type, supportedCapabilities: capabilities)
} catch {
FxALog.error("Failure initializing device: \(error).")
}
}
internal func ensureCapabilities(capabilities: [DeviceCapability]) {
// This method is called by `FxAccountManager` on its own asynchronous queue, hence
// no wrapping in a `DispatchQueue.global().async`.
assert(!Thread.isMainThread)
do {
try account.ensureCapabilities(supportedCapabilities: capabilities)
} catch {
FxALog.error("Failure ensuring device capabilities: \(error).")
}
}
}
public enum DeviceEventOutgoing {
case sendTab(title: String, url: String)
}

Просмотреть файл

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import os.log
class FxALog {
private static let log = OSLog(
subsystem: Bundle.main.bundleIdentifier!,
category: "FxAccountManager"
)
internal static func info(_ msg: String) {
log(msg, type: .info)
}
internal static func debug(_ msg: String) {
log(msg, type: .debug)
}
internal static func error(_ msg: String) {
log(msg, type: .error)
}
private static func log(_ msg: String, type: OSLogType) {
os_log("%@", log: log, type: type, msg)
}
}

Просмотреть файл

@ -0,0 +1,568 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
public extension Notification.Name {
static let accountLoggedOut = Notification.Name("accountLoggedOut")
static let accountAuthProblems = Notification.Name("accountAuthProblems")
static let accountAuthenticated = Notification.Name("accountAuthenticated")
static let accountProfileUpdate = Notification.Name("accountProfileUpdate")
}
// swiftlint:disable type_body_length
open class FxaAccountManager {
let accountStorage: KeyChainAccountStorage
let config: FxAConfig
let deviceConfig: DeviceConfig
let applicationScopes: [String]
var acct: FxAccount?
var account: FxAccount? {
get { return acct }
set {
acct = newValue
if let acc = acct {
constellation = makeDeviceConstellation(account: acc)
}
}
}
var state: AccountState = AccountState.start
var profile: Profile?
var constellation: DeviceConstellation?
var latestOAuthStateParam: String?
/// Instanciate the account manager.
/// This class is intended to be long-lived within your app.
/// `keychainAccessGroup` is especially important if you are
/// using the manager in iOS App Extensions.
public required init(
config: FxAConfig,
deviceConfig: DeviceConfig,
applicationScopes: [String] = [OAuthScope.profile, OAuthScope.oldSync],
keychainAccessGroup: String? = nil
) {
self.config = config
self.deviceConfig = deviceConfig
self.applicationScopes = applicationScopes
accountStorage = KeyChainAccountStorage(keychainAccessGroup: keychainAccessGroup)
setupAuthExceptionsListener()
}
private lazy var statePersistenceCallback: FxAStatePersistenceCallback = {
FxAStatePersistenceCallback(manager: self)
}()
/// Starts the FxA account manager and advances the state machine.
/// It is required to call this method before doing anything else with the manager.
/// Note that as a result of this initialization, notifications such as `accountAuthenticated` might be
/// fired.
public func initialize(completionHandler: @escaping (Result<Void, Error>) -> Void) {
processEvent(event: .initialize) {
DispatchQueue.main.async { completionHandler(Result.success(())) }
}
}
/// Returns true the user is currently logged-in to an account, no matter if they need to reconnect or not.
public func hasAccount() -> Bool {
return state == .authenticatedWithProfile ||
state == .authenticatedNoProfile ||
state == .authenticationProblem
}
/// Returns true if the account needs re-authentication.
/// Your app should present the option to start a new OAuth flow.
public func accountNeedsReauth() -> Bool {
return state == .authenticationProblem
}
/// Begins a new authentication flow.
///
/// This function returns a URL string that the caller should open in a webview.
///
/// Once the user has confirmed the authorization grant, they will get redirected to `redirect_url`:
/// the caller must intercept that redirection, extract the `code` and `state` query parameters and call
/// `finishAuthentication(...)` to complete the flow.
public func beginAuthentication(completionHandler: @escaping (Result<URL, Error>) -> Void) {
FxALog.info("beginAuthentication")
DispatchQueue.global().async {
let result = self.updatingLatestAuthState { account in
try account.beginOAuthFlow(scopes: self.applicationScopes)
}
DispatchQueue.main.async { completionHandler(result) }
}
}
/// Begins a new pairing flow.
/// The pairing URL corresponds to the URL shown by the other pairing party,
/// scanned by your app QR code reader.
///
/// This function returns a URL string that the caller should open in a webview.
///
/// Once the user has confirmed the authorization grant, they will get redirected to `redirect_url`:
/// the caller must intercept that redirection, extract the `code` and `state` query parameters and call
/// `finishAuthentication(...)` to complete the flow.
public func beginPairingAuthentication(
pairingUrl: String,
completionHandler: @escaping (Result<URL, Error>) -> Void
) {
DispatchQueue.global().async {
let result = self.updatingLatestAuthState { account in
try account.beginPairingFlow(pairingUrl: pairingUrl, scopes: self.applicationScopes)
}
DispatchQueue.main.async { completionHandler(result) }
}
}
/// Run a "begin authentication" closure, extracting the returned `state` from the returned URL
/// and put it aside for later in `latestOAuthStateParam`.
/// Afterwards, in `finishAuthentication` we ensure that we are
/// finishing the correct (and same) authentication flow.
private func updatingLatestAuthState(_ beginFlowFn: (FxAccount) throws -> URL) -> Result<URL, Error> {
do {
let url = try beginFlowFn(requireAccount())
let comps = URLComponents(url: url, resolvingAgainstBaseURL: true)
latestOAuthStateParam = comps!.queryItems!.first(where: { $0.name == "state" })!.value
return .success(url)
} catch {
return .failure(error)
}
}
/// Finish an authentication flow.
///
/// If it succeeds, a `.accountAuthenticated` notification will get fired.
public func finishAuthentication(
authData: FxaAuthData,
completionHandler: @escaping (Result<Void, Error>) -> Void
) {
if latestOAuthStateParam == nil {
DispatchQueue.main.async { completionHandler(.failure(FirefoxAccountError.noExistingAuthFlow)) }
} else if authData.state != latestOAuthStateParam {
DispatchQueue.main.async { completionHandler(.failure(FirefoxAccountError.wrongAuthFlow)) }
} else { /* state == latestAuthState */
processEvent(event: .authenticated(authData: authData)) {
DispatchQueue.main.async { completionHandler(.success(())) }
}
}
}
/// Try to get an OAuth access token.
public func getAccessToken(scope: String, completionHandler: @escaping (Result<AccessTokenInfo, Error>) -> Void) {
do {
let tokenInfo = try requireAccount().getAccessToken(scope: scope)
DispatchQueue.main.async { completionHandler(.success(tokenInfo)) }
} catch {
DispatchQueue.main.async { completionHandler(.failure(error)) }
}
}
/// Refresh the user profile in the background.
///
/// If it succeeds, a `.accountProfileUpdate` notification will get fired.
public func refreshProfile() {
processEvent(event: .fetchProfile) {
// Do nothing
}
}
/// Get the user profile synchronously. It could be empty
/// because of network or authentication problems.
public func accountProfile() -> Profile? {
if state == .authenticatedWithProfile || state == .authenticationProblem {
return profile
}
return nil
}
/// Get the device constellation.
public func deviceConstellation() -> DeviceConstellation? {
return constellation
}
/// Log-out from the account.
/// The `.accountLoggedOut` notification will also get fired.
public func logout(completionHandler: @escaping (Result<Void, Error>) -> Void) {
processEvent(event: .logout) {
DispatchQueue.main.async { completionHandler(.success(())) }
}
}
let fxaFsmQueue = DispatchQueue(label: "com.mozilla.fxa-mgr-queue")
internal func processEvent(event: Event, completionHandler: @escaping () -> Void) {
fxaFsmQueue.async {
var toProcess: Event? = event
while let e = toProcess {
guard let nextState = FxaAccountManager.nextState(state: self.state, event: e) else {
FxALog.error("Got invalid event \(e) for state \(self.state).")
continue
}
FxALog.debug("Processing event \(e) for state \(self.state). Next state is \(nextState).")
self.state = nextState
toProcess = self.stateActions(forState: self.state, via: e)
if let successiveEvent = toProcess {
FxALog.debug(
"Ran \(e) side-effects for state \(self.state), got successive event \(successiveEvent)."
)
}
}
completionHandler()
}
}
// State transition matrix. Returns nil if there's no transition.
internal static func nextState(state: AccountState, event: Event) -> AccountState? {
switch state {
case .start:
switch event {
case .initialize: return .start
case .accountNotFound: return .notAuthenticated
case .accountRestored: return .authenticatedNoProfile
default: return nil
}
case .notAuthenticated:
switch event {
case .authenticated: return .authenticatedNoProfile
default: return nil
}
case .authenticatedNoProfile:
switch event {
case .authenticationError: return .authenticationProblem
case .fetchProfile: return .authenticatedNoProfile
case .fetchedProfile: return .authenticatedWithProfile
case .failedToFetchProfile: return .authenticatedNoProfile
case .logout: return .notAuthenticated
default: return nil
}
case .authenticatedWithProfile:
switch event {
case .authenticationError: return .authenticationProblem
case .logout: return .notAuthenticated
default: return nil
}
case .authenticationProblem:
switch event {
case .recoveredFromAuthenticationProblem: return .authenticatedNoProfile
case .authenticated: return .authenticatedNoProfile
case .logout: return .notAuthenticated
default: return nil
}
}
}
// swiftlint:disable function_body_length
internal func stateActions(forState: AccountState, via: Event) -> Event? {
switch forState {
case .start: do {
switch via {
case .initialize: do {
if let acct = tryRestoreAccount() {
account = acct
return Event.accountRestored
} else {
return Event.accountNotFound
}
}
default: return nil
}
}
case .notAuthenticated: do {
switch via {
case .logout: do {
// Clean up internal account state and destroy the current FxA device record.
do {
try requireAccount().disconnect()
FxALog.info("Disconnected FxA account")
} catch {
FxALog.error("Failed to fully disconnect the FxA account: \(error).")
}
profile = nil
constellation = nil
accountStorage.clear()
// If we cannot instanciate FxA something is *really* wrong, crashing is a valid option.
account = createAccount()
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .accountLoggedOut,
object: nil
)
}
}
case .accountNotFound: do {
account = createAccount()
}
default: break // Do nothing
}
}
case .authenticatedNoProfile: do {
switch via {
case let .authenticated(authData): do {
FxALog.info("Registering persistence callback")
requireAccount().registerPersistCallback(statePersistenceCallback)
FxALog.debug("Completing oauth flow")
do {
try requireAccount().completeOAuthFlow(code: authData.code, state: authData.state)
} catch {
// Reasons this can fail:
// - network errors
// - unknown auth state
// - authenticating via web-content; we didn't beginOAuthFlowAsync
FxALog.error("Error completing OAuth flow: \(error)")
}
FxALog.info("Initializing device")
requireConstellation().initDevice(
name: deviceConfig.name,
type: deviceConfig.type,
capabilities: deviceConfig.capabilities
)
postAuthenticated(authType: authData.authType)
return Event.fetchProfile
}
case .accountRestored: do {
FxALog.info("Registering persistence callback")
requireAccount().registerPersistCallback(statePersistenceCallback)
FxALog.info("Ensuring device capabilities...")
requireConstellation().ensureCapabilities(capabilities: deviceConfig.capabilities)
postAuthenticated(authType: .existingAccount)
return Event.fetchProfile
}
case .recoveredFromAuthenticationProblem: do {
FxALog.info("Registering persistence callback")
requireAccount().registerPersistCallback(statePersistenceCallback)
FxALog.info("Initializing device")
requireConstellation().initDevice(
name: deviceConfig.name,
type: deviceConfig.type,
capabilities: deviceConfig.capabilities
)
postAuthenticated(authType: .recovered)
return Event.fetchProfile
}
case .fetchProfile: do {
// Profile fetching and account authentication issues:
// https://github.com/mozilla/application-services/issues/483
FxALog.info("Fetching profile...")
do {
profile = try requireAccount().getProfile()
} catch {
return Event.failedToFetchProfile
}
return Event.fetchedProfile
}
default: break // Do nothing
}
}
case .authenticatedWithProfile: do {
switch via {
case .fetchedProfile: do {
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .accountProfileUpdate,
object: nil,
userInfo: ["profile": self.profile!]
)
}
}
default: break // Do nothing
}
}
case .authenticationProblem:
switch via {
case .authenticationError: do {
// Somewhere in the system, we've just hit an authentication problem.
// There are two main causes:
// 1) an access token we've obtain from fxalib via 'getAccessToken' expired
// 2) password was changed, or device was revoked
// We can recover from (1) and test if we're in (2) by asking the fxalib.
// If it succeeds, then we can go back to whatever
// state we were in before. Future operations that involve access tokens should
// succeed.
func onError() {
// We are either certainly in the scenario (2), or were unable to determine
// our connectivity state. Let's assume we need to re-authenticate.
// This uncertainty about real state means that, hopefully rarely,
// we will disconnect users that hit transient network errors during
// an authorization check.
// See https://github.com/mozilla-mobile/android-components/issues/3347
FxALog.error("Unable to recover from an auth problem.")
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .accountAuthProblems,
object: nil
)
}
}
do {
let account = requireAccount()
let info = try account.checkAuthorizationStatus()
if !info.active {
onError()
return nil
}
try account.clearAccessTokenCache()
// Make sure we're back on track by re-requesting the profile access token.
_ = try account.getAccessToken(scope: OAuthScope.profile)
return .recoveredFromAuthenticationProblem
} catch {
onError()
}
return nil
}
default: break // Do nothing
}
}
return nil
}
internal func createAccount() -> FxAccount {
return try! FxAccount(config: config)
}
internal func tryRestoreAccount() -> FxAccount? {
return accountStorage.read()
}
internal func makeDeviceConstellation(account: FxAccount) -> DeviceConstellation {
return DeviceConstellation(account: account)
}
internal func postAuthenticated(authType: FxaAuthType) {
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .accountAuthenticated,
object: nil,
userInfo: ["authType": authType]
)
}
requireConstellation().refreshState()
}
// Handle auth exceptions caught in classes that don't hold a reference to the manager.
internal func setupAuthExceptionsListener() {
_ = NotificationCenter.default.addObserver(forName: .accountAuthException, object: nil, queue: nil) { _ in
self.processEvent(event: .authenticationError) {}
}
}
internal func requireAccount() -> FxAccount {
if let acct = account {
return acct
}
preconditionFailure("initialize() must be called first.")
}
internal func requireConstellation() -> DeviceConstellation {
if let cstl = constellation {
return cstl
}
preconditionFailure("account must be set (sets constellation).")
}
// swiftlint:enable function_body_length
}
// swiftlint:enable type_body_length
extension Notification.Name {
static let accountAuthException = Notification.Name("accountAuthException")
}
class FxAStatePersistenceCallback: PersistCallback {
weak var manager: FxaAccountManager?
public init(manager: FxaAccountManager) {
self.manager = manager
}
func persist(json: String) {
manager?.accountStorage.write(json)
}
}
/**
* States of the [FxaAccountManager].
*/
internal enum AccountState {
case start
case notAuthenticated
case authenticationProblem
case authenticatedNoProfile
case authenticatedWithProfile
}
/**
* Base class for [FxaAccountManager] state machine events.
* Events aren't a simple enum class because we might want to pass data along with some of the events.
*/
internal enum Event {
case initialize
case accountNotFound
case accountRestored
case authenticated(authData: FxaAuthData)
case authenticationError /* (error: AuthException) */
case recoveredFromAuthenticationProblem
case fetchProfile
case fetchedProfile
case failedToFetchProfile
case logout
}
public enum FxaAuthType {
case existingAccount
case signin
case signup
case pairing
case recovered
case other(reason: String)
internal static func fromActionQueryParam(_ action: String) -> FxaAuthType {
switch action {
case "signin": return .signin
case "signup": return .signup
case "pairing": return .pairing
default: return .other(reason: action)
}
}
}
public struct FxaAuthData {
public let code: String
public let state: String
public let authType: FxaAuthType
/// These constructor paramers shall be extracted from the OAuth final redirection URL query
/// parameters.
public init(code: String, state: String, actionQueryParam: String) {
self.code = code
self.state = state
authType = FxaAuthType.fromActionQueryParam(actionQueryParam)
}
}
public struct DeviceConfig {
let name: String
let type: DeviceType
let capabilities: [DeviceCapability]
public init(name: String, type: DeviceType, capabilities: [DeviceCapability]) {
self.name = name
self.type = type
self.capabilities = capabilities
}
}

Просмотреть файл

@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
public enum OAuthScope {
// Necessary to fetch a profile.
public static let profile: String = "profile"
// Necessary to obtain sync keys.
public static let oldSync: String = "https://identity.mozilla.com/apps/oldsync"
}
public struct ScopedKey {
public let kty: String
public let scope: String
public let k: String
public let kid: String
internal init(msg: MsgTypes_ScopedKey) {
kty = msg.kty
scope = msg.scope
k = msg.k
kid = msg.kid
}
}
public struct AccessTokenInfo {
public let scope: String
public let token: String
public let key: ScopedKey?
public let expiresAt: Date
internal init(msg: MsgTypes_AccessTokenInfo) {
scope = msg.scope
token = msg.token
key = msg.hasKey ? ScopedKey(msg: msg.key) : nil
expiresAt = Date(timeIntervalSince1970: Double(msg.expiresAt))
}
// For testing.
internal init(
scope: String,
token: String,
key: ScopedKey? = nil,
expiresAt: Date = Date()
) {
self.scope = scope
self.token = token
self.key = key
self.expiresAt = expiresAt
}
}
public struct IntrospectInfo {
public let active: Bool
public let tokenType: String
public let scope: String?
public let exp: Date
public let iss: String?
internal init(msg: MsgTypes_IntrospectInfo) {
active = msg.active
tokenType = msg.tokenType
scope = msg.hasScope ? msg.scope : nil
exp = Date(timeIntervalSince1970: Double(msg.exp))
iss = msg.hasIss ? msg.iss : nil
}
// For testing.
internal init(
active: Bool,
tokenType: String,
scope: String? = nil,
exp: Date = Date(),
iss: String? = nil
) {
self.active = active
self.tokenType = tokenType
self.scope = scope
self.exp = exp
self.iss = iss
}
}

Просмотреть файл

@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
public struct Avatar {
public let url: String
public let isDefault: Bool
}
public struct Profile {
public let uid: String
public let email: String
public let avatar: Avatar?
public let displayName: String?
internal init(msg: MsgTypes_Profile) {
uid = msg.uid
email = msg.email
avatar = msg.hasAvatar ? Avatar(url: msg.avatar, isDefault: msg.avatarDefault) : nil
displayName = msg.hasDisplayName ? msg.displayName : nil
}
// For testing.
internal init(uid: String, email: String, avatar: Avatar? = nil, displayName: String? = nil) {
self.uid = uid
self.email = email
self.avatar = avatar
self.displayName = displayName
}
}

Просмотреть файл

@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import SwiftKeychainWrapper
class KeyChainAccountStorage {
internal var keychainWrapper: KeychainWrapper
internal static var keychainKey: String = "accountJSON"
init(keychainAccessGroup: String?) {
keychainWrapper = KeychainWrapper.sharedAppContainerKeychain(keychainAccessGroup: keychainAccessGroup)
}
func read() -> FxAccount? {
if let json = keychainWrapper.string(forKey: KeyChainAccountStorage.keychainKey) {
do {
return try FxAccount(fromJsonState: json)
} catch {
FxALog.error("FxAccount internal state de-serialization failed: \(error).")
return nil
}
}
return nil
}
func write(_ json: String) {
if !keychainWrapper.set(json, forKey: KeyChainAccountStorage.keychainKey) {
FxALog.error("Could not write account state.")
}
}
func clear() {
if !keychainWrapper.removeObject(forKey: KeyChainAccountStorage.keychainKey) {
FxALog.error("Could not clear account state.")
}
}
}

Просмотреть файл

@ -47,6 +47,11 @@ char *_Nullable fxa_begin_oauth_flow(FirefoxAccountHandle handle,
const char *_Nonnull scopes,
FxAError *_Nonnull out);
char *_Nullable fxa_begin_pairing_flow(FirefoxAccountHandle handle,
const char *_Nonnull pairing_url,
const char *_Nonnull scopes,
FxAError *_Nonnull out);
void fxa_complete_oauth_flow(FirefoxAccountHandle handle,
const char *_Nonnull code,
const char *_Nonnull state,
@ -80,6 +85,51 @@ FxARustBuffer fxa_profile(FirefoxAccountHandle handle,
bool ignore_cache,
FxAError *_Nonnull out);
FxARustBuffer fxa_get_devices(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
FxARustBuffer fxa_poll_device_commands(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
FxARustBuffer fxa_handle_push_message(FirefoxAccountHandle handle,
const char *_Nonnull payload,
FxAError *_Nonnull out);
void fxa_send_tab(FirefoxAccountHandle handle,
const char *_Nonnull targetId,
const char *_Nonnull title,
const char *_Nonnull url,
FxAError *_Nonnull out);
void fxa_set_device_name(FirefoxAccountHandle handle,
const char *_Nonnull displayName,
FxAError *_Nonnull out);
void fxa_set_push_subscription(FirefoxAccountHandle handle,
const char *_Nonnull endpoint,
const char *_Nonnull publicKey,
const char *_Nonnull authKey,
FxAError *_Nonnull out);
void fxa_initialize_device(FirefoxAccountHandle handle,
const char *_Nonnull name,
int32_t device_type,
uint8_t const *_Nonnull capabilities_ptr,
int32_t capabilities_len,
FxAError *_Nonnull out);
void fxa_ensure_capabilities(FirefoxAccountHandle handle,
uint8_t const *_Nonnull capabilities_ptr,
int32_t capabilities_len,
FxAError *_Nonnull out);
void fxa_migrate_from_session_token(FirefoxAccountHandle handle,
const char *_Nonnull sessionToken,
const char *_Nonnull kSync,
const char *_Nonnull kXCS,
bool copySessionToken,
FxAError *_Nonnull out);
char *_Nullable fxa_get_token_server_endpoint_url(FirefoxAccountHandle handle,
FxAError *_Nonnull out);

Просмотреть файл

@ -0,0 +1,263 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import SwiftProtobuf
/// This class wraps Rust calls safely and performs necessary type conversions.
open class RustFxAccount {
private let raw: UInt64
internal init(raw: UInt64) {
self.raw = raw
}
/// Create a `RustFxAccount` from scratch. This is suitable for callers using the
/// OAuth Flow.
public required convenience init(config: FxAConfig) throws {
let pointer = try rustCall { err in
fxa_new(config.contentUrl, config.clientId, config.redirectUri, err)
}
self.init(raw: pointer)
}
/// Restore a previous instance of `RustFxAccount` from a serialized state (obtained with `toJSON(...)`).
public required convenience init(fromJsonState state: String) throws {
let pointer = try rustCall { err in fxa_from_json(state, err) }
self.init(raw: pointer)
}
deinit {
if self.raw != 0 {
try! rustCall { err in
// Is `try!` the right thing to do? We should only hit an error here
// for panics and handle misuse, both inidicate bugs in our code
// (the first in the rust code, the 2nd in this swift wrapper).
fxa_free(self.raw, err)
}
}
}
/// Serializes the state of a `RustFxAccount` instance. It can be restored
/// later with `fromJSON(...)`. It is the responsability of the caller to
/// persist that serialized state regularly (after operations that mutate
/// `RustFxAccount`) in a **secure** location.
open func toJSON() throws -> String {
let ptr = try rustCall { err in fxa_to_json(self.raw, err) }
return String(freeingFxaString: ptr)
}
/// Gets the logged-in user profile.
/// Throws `FirefoxAccountError.Unauthorized` if we couldn't find any suitable access token
/// to make that call. The caller should then start the OAuth Flow again with
/// the "profile" scope.
open func getProfile() throws -> Profile {
let ptr = try rustCall { err in
fxa_profile(self.raw, false, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_Profile(serializedData: Data(rustBuffer: ptr))
return Profile(msg: msg)
}
open func getTokenServerEndpointURL() throws -> URL {
let ptr = try rustCall { err in
fxa_get_token_server_endpoint_url(self.raw, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
open func getConnectionSuccessURL() throws -> URL {
let ptr = try rustCall { err in
fxa_get_connection_success_url(self.raw, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
open func getManageAccountURL(entrypoint: String) throws -> URL {
let ptr = try rustCall { err in
fxa_get_manage_account_url(self.raw, entrypoint, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
open func getManageDevicesURL(entrypoint: String) throws -> URL {
let ptr = try rustCall { err in
fxa_get_manage_devices_url(self.raw, entrypoint, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
/// Request a OAuth token by starting a new OAuth flow.
///
/// This function returns a URL string that the caller should open in a webview.
///
/// Once the user has confirmed the authorization grant, they will get redirected to `redirect_url`:
/// the caller must intercept that redirection, extract the `code` and `state` query parameters and call
/// `completeOAuthFlow(...)` to complete the flow.
open func beginOAuthFlow(scopes: [String]) throws -> URL {
let scope = scopes.joined(separator: " ")
let ptr = try rustCall { err in
fxa_begin_oauth_flow(self.raw, scope, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
open func beginPairingFlow(pairingUrl: String, scopes: [String]) throws -> URL {
let scope = scopes.joined(separator: " ")
let ptr = try rustCall { err in
fxa_begin_pairing_flow(self.raw, pairingUrl, scope, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
/// Finish an OAuth flow initiated by `beginOAuthFlow(...)` and returns token/keys.
///
/// This resulting token might not have all the `scopes` the caller have requested (e.g. the user
/// might have denied some of them): it is the responsibility of the caller to accomodate that.
open func completeOAuthFlow(code: String, state: String) throws {
try rustCall { err in
fxa_complete_oauth_flow(self.raw, code, state, err)
}
}
/// Try to get an OAuth access token.
///
/// Throws `FirefoxAccountError.Unauthorized` if we couldn't provide an access token
/// for this scope. The caller should then start the OAuth Flow again with
/// the desired scope.
open func getAccessToken(scope: String) throws -> AccessTokenInfo {
let ptr = try rustCall { err in
fxa_get_access_token(self.raw, scope, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_AccessTokenInfo(serializedData: Data(rustBuffer: ptr))
return AccessTokenInfo(msg: msg)
}
/// Check whether the refreshToken is active
open func checkAuthorizationStatus() throws -> IntrospectInfo {
let ptr = try rustCall { err in
fxa_check_authorization_status(self.raw, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_IntrospectInfo(serializedData: Data(rustBuffer: ptr))
return IntrospectInfo(msg: msg)
}
/// This method should be called when a request made with
/// an OAuth token failed with an authentication error.
/// It clears the internal cache of OAuth access tokens,
/// so the caller can try to call `getAccessToken` or `getProfile`
/// again.
open func clearAccessTokenCache() throws {
try rustCall { err in
fxa_clear_access_token_cache(self.raw, err)
}
}
/// Disconnect from the account and optionaly destroy our device record.
/// `beginOAuthFlow(...)` will need to be called to reconnect.
open func disconnect() throws {
try rustCall { err in
fxa_disconnect(self.raw, err)
}
}
open func fetchDevices() throws -> [Device] {
let ptr = try rustCall { err in
fxa_get_devices(self.raw, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_Devices(serializedData: Data(rustBuffer: ptr))
return Device.fromCollectionMsg(msg: msg)
}
open func setDeviceDisplayName(_ name: String) throws {
try rustCall { err in
fxa_set_device_name(self.raw, name, err)
}
}
open func pollDeviceCommands() throws -> [DeviceEvent] {
let ptr = try rustCall { err in
fxa_poll_device_commands(self.raw, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_AccountEvents(serializedData: Data(rustBuffer: ptr))
return DeviceEvent.fromCollectionMsg(msg: msg)
}
open func handlePushMessage(payload: String) throws -> [DeviceEvent] {
let ptr = try rustCall { err in
fxa_handle_push_message(self.raw, payload, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_AccountEvents(serializedData: Data(rustBuffer: ptr))
return DeviceEvent.fromCollectionMsg(msg: msg)
}
open func sendSingleTab(targetId: String, title: String, url: String) throws {
try rustCall { err in
fxa_send_tab(self.raw, targetId, title, url, err)
}
}
open func setDevicePushSubscription(endpoint: String, publicKey: String, authKey: String) throws {
try rustCall { err in
fxa_set_push_subscription(self.raw, endpoint, publicKey, authKey, err)
}
}
open func initializeDevice(
name: String,
deviceType: DeviceType,
supportedCapabilities: [DeviceCapability]
) throws {
let (data, size) = msgToBuffer(msg: supportedCapabilities.toCollectionMsg())
try data.withUnsafeBytes { bytes in
try rustCall { err in
fxa_initialize_device(
self.raw,
name,
Int32(deviceType.toMsg().rawValue),
bytes.bindMemory(to: UInt8.self).baseAddress!,
size,
err
)
}
}
}
open func ensureCapabilities(supportedCapabilities: [DeviceCapability]) throws {
let (data, size) = msgToBuffer(msg: supportedCapabilities.toCollectionMsg())
try data.withUnsafeBytes { bytes in
try rustCall { err in
fxa_ensure_capabilities(
self.raw,
bytes.bindMemory(to: UInt8.self).baseAddress!,
size,
err
)
}
}
}
private func msgToBuffer(msg: SwiftProtobuf.Message) -> (Data, Int32) {
let data = try! msg.serializedData()
let size = Int32(data.count)
return (data, size)
}
}
// This queue serves as a semaphore to the rust layer.
private let fxaRustQueue = DispatchQueue(label: "com.mozilla.fxa-rust")
internal func rustCall<T>(_ cb: (UnsafeMutablePointer<FxAError>) throws -> T?) throws -> T {
return try FirefoxAccountError.unwrap { err in
try fxaRustQueue.sync {
try cb(err)
}
}
}

Просмотреть файл

@ -76,6 +76,7 @@ No additional Kotlin dependencies should be added to the project unless absolute
We currently depend only on the [following dependencies](https://github.com/mozilla/application-services/blob/master/Cartfile):
* [swift-protobuf](https://github.com/apple/swift-protobuf)
* [SwiftKeychainWrapper](https://github.com/jrendel/SwiftKeychainWrapper/)
No additional Swift dependencies should be added to the project unless absolutely necessary.

Просмотреть файл

@ -6,6 +6,7 @@ the details of which are reproduced below.
* [Mozilla Public License 2.0](#mozilla-public-license-20)
* [Apache License 2.0](#apache-license-20)
* [MIT License: SwiftKeychainWrapper](#mit-license-swiftkeychainwrapper)
* [MIT License: aho-corasick, byteorder, memchr, termcolor](#mit-license-aho-corasick-byteorder-memchr-termcolor)
* [MIT License: ansi_term](#mit-license-ansi_term)
* [MIT License: atty](#mit-license-atty)
@ -734,6 +735,37 @@ 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.
```
-------------
## MIT License: SwiftKeychainWrapper
The following text applies to code linked from these dependencies:
[SwiftKeychainWrapper](https://github.com/jrendel/SwiftKeychainWrapper)
```
The MIT License (MIT)
Copyright (c) 2014 Jason Rendel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
-------------
## MIT License: aho-corasick, byteorder, memchr, termcolor

Просмотреть файл

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import SwiftKeychainWrapper
extension KeychainWrapper {
/// Return the base bundle identifier.
///
/// This function is smart enough to find out if it is being called from an extension or the main application. In
/// case of the former, it will chop off the extension identifier from the bundle since that is a suffix not part
/// of the *base* bundle identifier.
static var baseBundleIdentifier: String {
let bundle = Bundle.main
let packageType = bundle.object(forInfoDictionaryKey: "CFBundlePackageType") as? String
let baseBundleIdentifier = bundle.bundleIdentifier!
if packageType == "XPC!" {
let components = baseBundleIdentifier.components(separatedBy: ".")
return components[0 ..< components.count - 1].joined(separator: ".")
}
return baseBundleIdentifier
}
static var shared: KeychainWrapper?
static func sharedAppContainerKeychain(keychainAccessGroup: String?) -> KeychainWrapper {
if let s = shared {
return s
}
let wrapper = KeychainWrapper(serviceName: baseBundleIdentifier, accessGroup: keychainAccessGroup)
shared = wrapper
return wrapper
}
}

Просмотреть файл

@ -31,8 +31,23 @@
CDC0089F2236CAD100893800 /* places_msg_types.proto in Sources */ = {isa = PBXBuildFile; fileRef = CDC0089E2236CAD100893800 /* places_msg_types.proto */; };
CDC21B14221DCE3700AA71E5 /* RustLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC21B12221DCE3700AA71E5 /* RustLog.swift */; };
CDC21B15221DCE3700AA71E5 /* RustLogFFI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC21B13221DCE3700AA71E5 /* RustLogFFI.h */; settings = {ATTRIBUTES = (Public, ); }; };
CE1445AC23D6058B00B1E808 /* FxAccountConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1445AB23D6058B00B1E808 /* FxAccountConfig.swift */; };
CE1445AF23D6315200B1E808 /* FxAccountLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1445AE23D6315200B1E808 /* FxAccountLogging.swift */; };
CE1ADA9722249FDA00E89714 /* Data+RustBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1ADA9622249FDA00E89714 /* Data+RustBuffer.swift */; };
CE1B09A3231863D7006226E1 /* KeychainWrapper+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1B09A2231863D7006226E1 /* KeychainWrapper+.swift */; };
CE1B09A5231865BC006226E1 /* FxAccountStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1B09A4231865BC006226E1 /* FxAccountStorage.swift */; };
CE2D04C5231822AC00AF5722 /* FxAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D04C4231822AC00AF5722 /* FxAccountManager.swift */; };
CE3A2F38225BDE5300EA569C /* PlacesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3A2F37225BDE5300EA569C /* PlacesTests.swift */; };
CE90D1BB23D7570A00FD9A5F /* FxAccountManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE34DB3C23D0C9640027AD63 /* FxAccountManagerTests.swift */; };
CE96459923D7A77500B662F8 /* SwiftKeychainWrapper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE96459823D7A77500B662F8 /* SwiftKeychainWrapper.framework */; };
CE96459C23D7A7D500B662F8 /* SwiftKeychainWrapper.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE96459823D7A77500B662F8 /* SwiftKeychainWrapper.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CEB1A06823D8A42D005BD4DD /* FxAccountMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB1A06723D8A42D005BD4DD /* FxAccountMocks.swift */; };
CED443CB23CCF385007E5FA6 /* FxAccountDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED443CA23CCF385007E5FA6 /* FxAccountDevice.swift */; };
CED443CD23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED443CC23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift */; };
CEDDBC8C23DB438F00CFF5AA /* RustFxAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC8B23DB438F00CFF5AA /* RustFxAccount.swift */; };
CEDDBC8E23DB4CD600CFF5AA /* FxAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC8D23DB4CD600CFF5AA /* FxAccount.swift */; };
CEDDBC9023DB4F7400CFF5AA /* FxAccountOAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC8F23DB4F7400CFF5AA /* FxAccountOAuth.swift */; };
CEDDBC9223DB4F9A00CFF5AA /* FxAccountProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC9123DB4F9A00CFF5AA /* FxAccountProfile.swift */; };
CEFB1EB022EF708B0001E20F /* ResultError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFB1EAF22EF708B0001E20F /* ResultError.swift */; };
D05434A1225680D900FDE4EF /* MozillaAppServices.h in Headers */ = {isa = PBXBuildFile; fileRef = C852EEF2220A3C6800A6E79A /* MozillaAppServices.h */; settings = {ATTRIBUTES = (Public, ); }; };
D05434A2225680F400FDE4EF /* RustFxAFFI.h in Headers */ = {isa = PBXBuildFile; fileRef = C852EEE0220A2A2B00A6E79A /* RustFxAFFI.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -48,6 +63,8 @@
compilerSpec = com.apple.compilers.proxy.script;
filePatterns = "*.proto";
fileType = pattern.proxy;
inputFiles = (
);
isEditable = 1;
outputFiles = (
"$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).pb.swift",
@ -73,6 +90,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
CE96459C23D7A7D500B662F8 /* SwiftKeychainWrapper.framework in CopyFiles */,
CD440A452238A703003F004B /* SwiftProtobuf.framework in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -106,9 +124,23 @@
CDC0089E2236CAD100893800 /* places_msg_types.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; name = places_msg_types.proto; path = ../../src/places_msg_types.proto; sourceTree = "<group>"; };
CDC21B12221DCE3700AA71E5 /* RustLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RustLog.swift; sourceTree = "<group>"; };
CDC21B13221DCE3700AA71E5 /* RustLogFFI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RustLogFFI.h; sourceTree = "<group>"; };
CE1445AB23D6058B00B1E808 /* FxAccountConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountConfig.swift; sourceTree = "<group>"; };
CE1445AE23D6315200B1E808 /* FxAccountLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountLogging.swift; sourceTree = "<group>"; };
CE1ADA9622249FDA00E89714 /* Data+RustBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+RustBuffer.swift"; sourceTree = "<group>"; };
CE1B09A2231863D7006226E1 /* KeychainWrapper+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainWrapper+.swift"; sourceTree = "<group>"; };
CE1B09A4231865BC006226E1 /* FxAccountStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountStorage.swift; sourceTree = "<group>"; };
CE2D04C4231822AC00AF5722 /* FxAccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAccountManager.swift; sourceTree = "<group>"; };
CE34DB3C23D0C9640027AD63 /* FxAccountManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountManagerTests.swift; sourceTree = "<group>"; };
CE3A2F37225BDE5300EA569C /* PlacesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesTests.swift; sourceTree = "<group>"; };
CE96459823D7A77500B662F8 /* SwiftKeychainWrapper.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftKeychainWrapper.framework; path = ../../Carthage/Build/iOS/SwiftKeychainWrapper.framework; sourceTree = "<group>"; };
CE9D202020914D0D00F1C8FA /* MozillaAppServices.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MozillaAppServices.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CEB1A06723D8A42D005BD4DD /* FxAccountMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountMocks.swift; sourceTree = "<group>"; };
CED443CA23CCF385007E5FA6 /* FxAccountDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountDevice.swift; sourceTree = "<group>"; };
CED443CC23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountDeviceConstellation.swift; sourceTree = "<group>"; };
CEDDBC8B23DB438F00CFF5AA /* RustFxAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustFxAccount.swift; sourceTree = "<group>"; };
CEDDBC8D23DB4CD600CFF5AA /* FxAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccount.swift; sourceTree = "<group>"; };
CEDDBC8F23DB4F7400CFF5AA /* FxAccountOAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountOAuth.swift; sourceTree = "<group>"; };
CEDDBC9123DB4F9A00CFF5AA /* FxAccountProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountProfile.swift; sourceTree = "<group>"; };
CEFB1EAF22EF708B0001E20F /* ResultError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultError.swift; sourceTree = "<group>"; };
EB7DE84C2214D30B00E7CF17 /* SwiftProtobuf.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftProtobuf.framework; path = ../../Carthage/Build/iOS/SwiftProtobuf.framework; sourceTree = "<group>"; };
EB879D7A221234EB00753DC9 /* MozillaAppServicesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MozillaAppServicesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -124,6 +156,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CE96459923D7A77500B662F8 /* SwiftKeychainWrapper.framework in Frameworks */,
C852EEEF220A2E9400A6E79A /* libmegazord_ios.a in Frameworks */,
EB7DE84D2214D30B00E7CF17 /* SwiftProtobuf.framework in Frameworks */,
C852EE9E220A283200A6E79A /* libsqlcipher.a in Frameworks */,
@ -188,6 +221,16 @@
children = (
CDC0089C2236CAB900893800 /* fxa_msg_types.proto */,
C852EEDD220A2A2B00A6E79A /* FirefoxAccount.swift */,
CEDDBC8B23DB438F00CFF5AA /* RustFxAccount.swift */,
CEDDBC8D23DB4CD600CFF5AA /* FxAccount.swift */,
CE1445AB23D6058B00B1E808 /* FxAccountConfig.swift */,
CED443CA23CCF385007E5FA6 /* FxAccountDevice.swift */,
CED443CC23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift */,
CE1445AE23D6315200B1E808 /* FxAccountLogging.swift */,
CE2D04C4231822AC00AF5722 /* FxAccountManager.swift */,
CEDDBC8F23DB4F7400CFF5AA /* FxAccountOAuth.swift */,
CEDDBC9123DB4F9A00CFF5AA /* FxAccountProfile.swift */,
CE1B09A4231865BC006226E1 /* FxAccountStorage.swift */,
C852EEDE220A2A2B00A6E79A /* Extensions */,
C852EEE0220A2A2B00A6E79A /* RustFxAFFI.h */,
C852EEE2220A2A2B00A6E79A /* Errors */,
@ -253,11 +296,20 @@
path = RustLog;
sourceTree = "<group>";
};
CE1B099D231862D5006226E1 /* Extensions */ = {
isa = PBXGroup;
children = (
CE1B09A2231863D7006226E1 /* KeychainWrapper+.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
CE9D201620914D0D00F1C8FA = {
isa = PBXGroup;
children = (
C852EEF2220A3C6800A6E79A /* MozillaAppServices.h */,
CD02CF3822568BCC00124DA2 /* SyncUnlockInfo.swift */,
CE1B099D231862D5006226E1 /* Extensions */,
CEFB1EB122EF70960001E20F /* Errors */,
CD85A44822361E880099BFA9 /* Places */,
CDC21B11221DCE3700AA71E5 /* RustLog */,
@ -282,6 +334,7 @@
CE9D203720914D4800F1C8FA /* Frameworks */ = {
isa = PBXGroup;
children = (
CE96459823D7A77500B662F8 /* SwiftKeychainWrapper.framework */,
EB7DE84C2214D30B00E7CF17 /* SwiftProtobuf.framework */,
C852EE9D220A283200A6E79A /* libsqlcipher.a */,
C852EEEE220A2E9400A6E79A /* libmegazord_ios.a */,
@ -301,6 +354,8 @@
isa = PBXGroup;
children = (
EB879D7E221234EB00753DC9 /* Info.plist */,
CEB1A06723D8A42D005BD4DD /* FxAccountMocks.swift */,
CE34DB3C23D0C9640027AD63 /* FxAccountManagerTests.swift */,
EB879D8A22123FD900753DC9 /* LoginsTests.swift */,
CD4CFDD2221DFA5100EB3B33 /* LogTest.swift */,
CE3A2F37225BDE5300EA569C /* PlacesTests.swift */,
@ -442,16 +497,26 @@
buildActionMask = 2147483647;
files = (
CEFB1EB022EF708B0001E20F /* ResultError.swift in Sources */,
CE1B09A5231865BC006226E1 /* FxAccountStorage.swift in Sources */,
CEDDBC9023DB4F7400CFF5AA /* FxAccountOAuth.swift in Sources */,
CE1ADA9722249FDA00E89714 /* Data+RustBuffer.swift in Sources */,
C852EEE8220A2A2B00A6E79A /* String+Free_FxAClient.swift in Sources */,
CDC0089F2236CAD100893800 /* places_msg_types.proto in Sources */,
CEDDBC8E23DB4CD600CFF5AA /* FxAccount.swift in Sources */,
CEDDBC8C23DB438F00CFF5AA /* RustFxAccount.swift in Sources */,
CED443CD23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift in Sources */,
CEDDBC9223DB4F9A00CFF5AA /* FxAccountProfile.swift in Sources */,
C852EEEB220A2A2B00A6E79A /* FxAError.swift in Sources */,
C852EED7220A29FE00A6E79A /* LoginStoreError.swift in Sources */,
CD02CF3922568BCC00124DA2 /* SyncUnlockInfo.swift in Sources */,
CDC0089D2236CAB900893800 /* fxa_msg_types.proto in Sources */,
CD85A45A22361E890099BFA9 /* PlacesError.swift in Sources */,
CE1445AC23D6058B00B1E808 /* FxAccountConfig.swift in Sources */,
CD85A45422361E890099BFA9 /* Places.swift in Sources */,
CED443CB23CCF385007E5FA6 /* FxAccountDevice.swift in Sources */,
C852EED8220A29FE00A6E79A /* LockError.swift in Sources */,
CE2D04C5231822AC00AF5722 /* FxAccountManager.swift in Sources */,
CE1B09A3231863D7006226E1 /* KeychainWrapper+.swift in Sources */,
CD85A45722361E890099BFA9 /* String+Free_Places.swift in Sources */,
C852EED5220A29FE00A6E79A /* LoginRecord.swift in Sources */,
C852EEE7220A2A2B00A6E79A /* FirefoxAccount.swift in Sources */,
@ -459,6 +524,7 @@
CDC21B14221DCE3700AA71E5 /* RustLog.swift in Sources */,
C852EED6220A29FE00A6E79A /* LoginsStorage.swift in Sources */,
C852EED3220A29FE00A6E79A /* String+Free_Logins.swift in Sources */,
CE1445AF23D6315200B1E808 /* FxAccountLogging.swift in Sources */,
CD85A45922361E890099BFA9 /* Bookmark.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -468,8 +534,10 @@
buildActionMask = 2147483647;
files = (
CE3A2F38225BDE5300EA569C /* PlacesTests.swift in Sources */,
CEB1A06823D8A42D005BD4DD /* FxAccountMocks.swift in Sources */,
CD4CFDD3221DFA5100EB3B33 /* LogTest.swift in Sources */,
EB879D8B22123FD900753DC9 /* LoginsTests.swift in Sources */,
CE90D1BB23D7570A00FD9A5F /* FxAccountManagerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

Просмотреть файл

@ -0,0 +1,277 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import XCTest
@testable import MozillaAppServices
class FxAccountManagerTests: XCTestCase {
func testStateTransitionsStart() {
let state: AccountState = .start
XCTAssertEqual(.start, FxaAccountManager.nextState(state: state, event: .initialize))
XCTAssertEqual(.notAuthenticated, FxaAccountManager.nextState(state: state, event: .accountNotFound))
XCTAssertEqual(.authenticatedNoProfile, FxaAccountManager.nextState(state: state, event: .accountRestored))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .authenticated(authData: FxaAuthData(code: "foo", state: "bar", actionQueryParam: "bobo"))))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .authenticationError))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .fetchProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .fetchedProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .failedToFetchProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .logout))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .recoveredFromAuthenticationProblem))
}
func testStateTransitionsNotAuthenticated() {
let state: AccountState = .notAuthenticated
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .initialize))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .accountNotFound))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .accountRestored))
XCTAssertEqual(.authenticatedNoProfile, FxaAccountManager.nextState(state: state, event: .authenticated(authData: FxaAuthData(code: "foo", state: "bar", actionQueryParam: "bobo"))))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .authenticationError))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .fetchProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .fetchedProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .failedToFetchProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .logout))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .recoveredFromAuthenticationProblem))
}
func testStateTransitionsAuthenticatedNoProfile() {
let state: AccountState = .authenticatedNoProfile
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .initialize))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .accountNotFound))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .accountRestored))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .authenticated(authData: FxaAuthData(code: "foo", state: "bar", actionQueryParam: "bobo"))))
XCTAssertEqual(.authenticationProblem, FxaAccountManager.nextState(state: state, event: .authenticationError))
XCTAssertEqual(.authenticatedNoProfile, FxaAccountManager.nextState(state: state, event: .fetchProfile))
XCTAssertEqual(.authenticatedWithProfile, FxaAccountManager.nextState(state: state, event: .fetchedProfile))
XCTAssertEqual(.authenticatedNoProfile, FxaAccountManager.nextState(state: state, event: .failedToFetchProfile))
XCTAssertEqual(.notAuthenticated, FxaAccountManager.nextState(state: state, event: .logout))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .recoveredFromAuthenticationProblem))
}
func testStateTransitionsAuthenticatedWithProfile() {
let state: AccountState = .authenticatedWithProfile
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .initialize))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .accountNotFound))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .accountRestored))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .authenticated(authData: FxaAuthData(code: "foo", state: "bar", actionQueryParam: "bobo"))))
XCTAssertEqual(.authenticationProblem, FxaAccountManager.nextState(state: state, event: .authenticationError))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .fetchProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .fetchedProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .failedToFetchProfile))
XCTAssertEqual(.notAuthenticated, FxaAccountManager.nextState(state: state, event: .logout))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .recoveredFromAuthenticationProblem))
}
func testStateTransitionsAuthenticationProblem() {
let state: AccountState = .authenticationProblem
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .initialize))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .accountNotFound))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .accountRestored))
XCTAssertEqual(.authenticatedNoProfile, FxaAccountManager.nextState(state: state, event: .authenticated(authData: FxaAuthData(code: "foo", state: "bar", actionQueryParam: "bobo"))))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .authenticationError))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .fetchProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .fetchedProfile))
XCTAssertNil(FxaAccountManager.nextState(state: state, event: .failedToFetchProfile))
XCTAssertEqual(.notAuthenticated, FxaAccountManager.nextState(state: state, event: .logout))
XCTAssertEqual(.authenticatedNoProfile, FxaAccountManager.nextState(state: state, event: .recoveredFromAuthenticationProblem))
}
func testAccountNotFound() {
let mgr = mockFxAManager()
let initDone = XCTestExpectation(description: "Initialization done")
mgr.initialize { _ in
initDone.fulfill()
}
wait(for: [initDone], timeout: 5)
let account = mgr.account as! MockFxAccount
let constellation = mgr.constellation as! MockDeviceConstellation
XCTAssertEqual(account.invocations, [])
XCTAssertEqual(constellation.invocations, [])
}
func testAccountRestoration() {
let mgr = mockFxAManager()
let account = MockFxAccount()
mgr.storedAccount = account
expectation(forNotification: .accountAuthenticated, object: nil, handler: nil)
expectation(forNotification: .accountProfileUpdate, object: nil, handler: nil)
let initDone = XCTestExpectation(description: "Initialization done")
mgr.initialize { _ in
initDone.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
// Fetch devices is run async, so it could happen after getProfile, hence we don't do a strict
// equality.
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.registerPersistCallback))
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.ensureCapabilities))
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.getProfile))
let constellation = mgr.constellation as! MockDeviceConstellation
XCTAssertEqual(constellation.invocations, [
MockDeviceConstellation.MethodInvocation.ensureCapabilities,
MockDeviceConstellation.MethodInvocation.refreshState,
])
}
func testAccountRestorationEnsureCapabilitiesNonAuthError() {
class MockAccount: MockFxAccount {
override func ensureCapabilities(supportedCapabilities _: [DeviceCapability]) throws {
throw FirefoxAccountError.network(message: "The WiFi cable is detached.")
}
}
let mgr = mockFxAManager()
let account = MockAccount()
mgr.storedAccount = account
expectation(forNotification: .accountAuthenticated, object: nil, handler: nil)
expectation(forNotification: .accountProfileUpdate, object: nil, handler: nil)
let initDone = XCTestExpectation(description: "Initialization done")
mgr.initialize { _ in
initDone.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.registerPersistCallback))
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.getProfile))
let constellation = mgr.constellation as! MockDeviceConstellation
XCTAssertEqual(constellation.invocations, [
MockDeviceConstellation.MethodInvocation.ensureCapabilities,
MockDeviceConstellation.MethodInvocation.refreshState,
])
}
func testAccountRestorationEnsureCapabilitiesAuthError() {
class MockAccount: MockFxAccount {
override func ensureCapabilities(supportedCapabilities _: [DeviceCapability]) throws {
notifyAuthError()
throw FirefoxAccountError.unauthorized(message: "Your token is expired yo.")
}
override func checkAuthorizationStatus() throws -> IntrospectInfo {
_ = try super.checkAuthorizationStatus()
return IntrospectInfo(active: false, tokenType: "refresh_token")
}
}
let mgr = mockFxAManager()
let account = MockAccount()
mgr.storedAccount = account
expectation(forNotification: .accountAuthenticated, object: nil, handler: nil)
expectation(forNotification: .accountProfileUpdate, object: nil, handler: nil)
expectation(forNotification: .accountAuthProblems, object: nil, handler: nil)
let initDone = XCTestExpectation(description: "Initialization done")
mgr.initialize { _ in
initDone.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
XCTAssertTrue(mgr.accountNeedsReauth())
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.registerPersistCallback))
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.getProfile))
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.checkAuthorizationStatus))
let constellation = mgr.constellation as! MockDeviceConstellation
XCTAssertEqual(constellation.invocations, [
MockDeviceConstellation.MethodInvocation.ensureCapabilities,
MockDeviceConstellation.MethodInvocation.refreshState,
])
}
func testNewAccountLogIn() {
let mgr = mockFxAManager()
let beginAuthDone = XCTestExpectation(description: "beginAuthDone")
var authURL: String?
mgr.initialize { _ in
mgr.beginAuthentication { url in
authURL = try! url.get().absoluteString
beginAuthDone.fulfill()
}
}
wait(for: [beginAuthDone], timeout: 5)
XCTAssertEqual(authURL, "https://foo.bar/oauth?state=bobo")
let finishAuthDone = XCTestExpectation(description: "finishAuthDone")
mgr.finishAuthentication(authData: FxaAuthData(code: "bobo", state: "bobo", actionQueryParam: "email")) { result in
if case .success = result {
finishAuthDone.fulfill()
}
}
wait(for: [finishAuthDone], timeout: 5)
let account = mgr.account! as! MockFxAccount
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.registerPersistCallback))
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.initializeDevice))
XCTAssertTrue(account.invocations.contains(MockFxAccount.MethodInvocation.getProfile))
let constellation = mgr.constellation as! MockDeviceConstellation
XCTAssertEqual(constellation.invocations, [
MockDeviceConstellation.MethodInvocation.initDevice,
MockDeviceConstellation.MethodInvocation.refreshState,
])
}
func testAuthStateVerification() {
let mgr = mockFxAManager()
let beginAuthDone = XCTestExpectation(description: "beginAuthDone")
var authURL: String?
mgr.initialize { _ in
mgr.beginAuthentication { url in
authURL = try! url.get().absoluteString
beginAuthDone.fulfill()
}
}
wait(for: [beginAuthDone], timeout: 5)
XCTAssertEqual(authURL, "https://foo.bar/oauth?state=bobo")
let finishAuthDone = XCTestExpectation(description: "finishAuthDone")
mgr.finishAuthentication(authData: FxaAuthData(code: "bobo", state: "NOTBOBO", actionQueryParam: "email")) { result in
if case .failure = result {
finishAuthDone.fulfill()
}
}
wait(for: [finishAuthDone], timeout: 5)
}
func testProfileRecoverableAuthError() {
class MockAccount: MockFxAccount {
var profileCallCount = 0
override func getProfile() throws -> Profile {
let profile = try super.getProfile()
profileCallCount += 1
if profileCallCount == 1 {
notifyAuthError()
throw FirefoxAccountError.unauthorized(message: "Uh oh.")
} else {
return profile
}
}
}
let mgr = mockFxAManager()
let account = MockAccount()
mgr.storedAccount = account
expectation(forNotification: .accountAuthenticated, object: nil, handler: nil)
expectation(forNotification: .accountProfileUpdate, object: nil, handler: nil)
let initDone = XCTestExpectation(description: "Initialization done")
mgr.initialize { _ in
initDone.fulfill()
}
waitForExpectations(timeout: 10, handler: nil)
XCTAssertFalse(mgr.accountNeedsReauth())
XCTAssertTrue(account.invocations.contains(MockAccount.MethodInvocation.checkAuthorizationStatus))
}
}

Просмотреть файл

@ -0,0 +1,126 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
@testable import MozillaAppServices
class MockFxAccount: FxAccount {
var invocations: [MethodInvocation] = []
enum MethodInvocation {
case checkAuthorizationStatus
case ensureCapabilities
case getProfile
case registerPersistCallback
case clearAccessTokenCache
case getAccessToken
case initializeDevice
case fetchDevices
}
init() {
super.init(raw: 0)
}
required convenience init(fromJsonState _: String) throws {
fatalError("init(fromJsonState:) has not been implemented")
}
required convenience init(config _: FxAConfig) throws {
fatalError("init(config:) has not been implemented")
}
override func initializeDevice(name _: String, deviceType _: DeviceType, supportedCapabilities _: [DeviceCapability]) throws {
invocations.append(.initializeDevice)
}
override func fetchDevices() throws -> [Device] {
invocations.append(.fetchDevices)
return []
}
override func registerPersistCallback(_: PersistCallback) {
invocations.append(.registerPersistCallback)
}
override func ensureCapabilities(supportedCapabilities _: [DeviceCapability]) throws {
invocations.append(.ensureCapabilities)
}
override func checkAuthorizationStatus() throws -> IntrospectInfo {
invocations.append(.checkAuthorizationStatus)
return IntrospectInfo(active: true, tokenType: "refresh_token")
}
override func clearAccessTokenCache() throws {
invocations.append(.clearAccessTokenCache)
}
override func getAccessToken(scope _: String) throws -> AccessTokenInfo {
invocations.append(.getAccessToken)
return AccessTokenInfo(scope: "profile", token: "toktok")
}
override func getProfile() throws -> Profile {
invocations.append(.getProfile)
return Profile(uid: "uid", email: "foo@bar.bobo")
}
override func beginOAuthFlow(scopes _: [String]) throws -> URL {
return URL(string: "https://foo.bar/oauth?state=bobo")!
}
}
class MockFxaAccountManager: FxaAccountManager {
var invocations: [MethodInvocation] = []
enum MethodInvocation {}
var storedAccount: FxAccount?
override func createAccount() -> FxAccount {
return MockFxAccount()
}
override func makeDeviceConstellation(account _: FxAccount) -> DeviceConstellation {
return MockDeviceConstellation(account: account)
}
override func tryRestoreAccount() -> FxAccount? {
return storedAccount
}
}
class MockDeviceConstellation: DeviceConstellation {
var invocations: [MethodInvocation] = []
enum MethodInvocation {
case ensureCapabilities
case initDevice
case refreshState
}
override init(account: FxAccount?) {
super.init(account: account ?? MockFxAccount())
}
override func initDevice(name: String, type: DeviceType, capabilities: [DeviceCapability]) {
invocations.append(.initDevice)
super.initDevice(name: name, type: type, capabilities: capabilities)
}
override func ensureCapabilities(capabilities: [DeviceCapability]) {
invocations.append(.ensureCapabilities)
super.ensureCapabilities(capabilities: capabilities)
}
override func refreshState() {
invocations.append(.refreshState)
super.refreshState()
}
}
func mockFxAManager() -> MockFxaAccountManager {
return MockFxaAccountManager(
config: .release(clientId: "clientid", redirectUri: "redirect"),
deviceConfig: DeviceConfig(name: "foo", type: .mobile, capabilities: [])
)
}

Просмотреть файл

@ -108,6 +108,12 @@ EXTRA_PACKAGE_METADATA = {
"license": "Apache-2.0",
"license_file": "https://raw.githubusercontent.com/apple/swift-protobuf/master/LICENSE.txt"
},
"ext-swift-keychain-wrapper": {
"name": "SwiftKeychainWrapper",
"repository": "https://github.com/jrendel/SwiftKeychainWrapper",
"license": "MIT",
"license_file": "https://raw.githubusercontent.com/jrendel/SwiftKeychainWrapper/develop/LICENSE"
},
"ext-nss": {
"name": "NSS",
"repository": "https://hg.mozilla.org/projects/nss",
@ -642,6 +648,7 @@ class WorkspaceMetadata(object):
extras.add("ext-protobuf")
if self.target_is_ios(target):
extras.add("ext-swift-protobuf")
extras.add("ext-swift-keychain-wrapper")
for dep in deps:
name = self.pkgInfoById[dep]["name"]
if name in PACKAGES_WITH_EXTRA_DEPENDENCIES: