-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathSyncAuthState.swift
153 lines (139 loc) · 6.85 KB
/
SyncAuthState.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* 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 Shared
import XCGLogger
import Deferred
import SwiftyJSON
private let CurrentSyncAuthStateCacheVersion = 1
private let log = Logger.syncLogger
public struct SyncAuthStateCache {
let token: TokenServerToken
let forKey: Data
let expiresAt: Timestamp
}
public protocol SyncAuthState {
func invalidate()
func token(_ now: Timestamp, canBeExpired: Bool) -> Deferred<Maybe<(token: TokenServerToken, forKey: Data)>>
var deviceID: String? { get }
var enginesEnablements: [String: Bool]? { get set }
var clientName: String? { get set }
}
public func syncAuthStateCachefromJSON(_ json: JSON) -> SyncAuthStateCache? {
if let version = json["version"].int {
if version != CurrentSyncAuthStateCacheVersion {
log.warning("Sync Auth State Cache is wrong version; dropping.")
return nil
}
if let
token = TokenServerToken.fromJSON(json["token"]),
let forKey = json["forKey"].string?.hexDecodedData,
let expiresAt = json["expiresAt"].int64 {
return SyncAuthStateCache(token: token, forKey: forKey, expiresAt: Timestamp(expiresAt))
}
}
return nil
}
extension SyncAuthStateCache: JSONLiteralConvertible {
public func asJSON() -> JSON {
return JSON([
"version": CurrentSyncAuthStateCacheVersion,
"token": token.asJSON(),
"forKey": forKey.hexEncodedString,
"expiresAt": NSNumber(value: expiresAt),
] as NSDictionary)
}
}
open class FirefoxAccountSyncAuthState: SyncAuthState {
fileprivate let account: FirefoxAccount
fileprivate let cache: KeychainCache<SyncAuthStateCache>
public var deviceID: String? {
return account.deviceRegistration?.id
}
public var enginesEnablements: [String : Bool]?
public var clientName: String?
init(account: FirefoxAccount, cache: KeychainCache<SyncAuthStateCache>) {
self.account = account
self.cache = cache
}
// If a token gives you a 401, invalidate it and request a new one.
open func invalidate() {
log.info("Invalidating cached token server token.")
self.cache.value = nil
}
// Generate an assertion and try to fetch a token server token, retrying at most a fixed number
// of times.
//
// It's tricky to get Swift to recurse into a closure that captures from the environment without
// segfaulting the compiler, so we pass everything around, like barbarians.
fileprivate func generateAssertionAndFetchTokenAt(_ audience: String,
client: TokenServerClient,
clientState: String?,
married: MarriedState,
now: Timestamp,
retryCount: Int) -> Deferred<Maybe<TokenServerToken>> {
let assertion = married.generateAssertionForAudience(audience, now: now)
return client.token(assertion, clientState: clientState).bind { result in
if retryCount > 0 {
if let tokenServerError = result.failureValue as? TokenServerError {
switch tokenServerError {
case let .remote(code, status, remoteTimestamp) where code == 401 && status == "invalid-timestamp":
if let remoteTimestamp = remoteTimestamp {
let skew = Int64(remoteTimestamp) - Int64(now) // Without casts, runtime crash due to overflow.
log.info("Token server responded with 401/invalid-timestamp: retrying with remote timestamp \(remoteTimestamp), which is local timestamp + skew = \(now) + \(skew).")
return self.generateAssertionAndFetchTokenAt(audience, client: client, clientState: clientState, married: married, now: remoteTimestamp, retryCount: retryCount - 1)
}
default:
break
}
}
}
// Fall-through.
return Deferred(value: result)
}
}
open func token(_ now: Timestamp, canBeExpired: Bool) -> Deferred<Maybe<(token: TokenServerToken, forKey: Data)>> {
if let value = cache.value {
// Give ourselves some room to do work.
let isExpired = value.expiresAt < now + 5 * OneMinuteInMilliseconds
if canBeExpired {
if isExpired {
log.info("Returning cached expired token.")
} else {
log.info("Returning cached token, which should be valid.")
}
return deferMaybe((token: value.token, forKey: value.forKey))
}
if !isExpired {
log.info("Returning cached token, which should be valid.")
return deferMaybe((token: value.token, forKey: value.forKey))
}
}
log.debug("Advancing Account state.")
return account.marriedState().bind { result in
if let married = result.successValue {
log.info("Account is in Married state; generating assertion.")
let tokenServerEndpointURL = self.account.configuration.sync15Configuration.tokenServerEndpointURL
let audience = TokenServerClient.getAudience(forURL: tokenServerEndpointURL)
let client = TokenServerClient(URL: tokenServerEndpointURL)
let clientState = married.kXCS
log.debug("Fetching token server token.")
let deferred = self.generateAssertionAndFetchTokenAt(audience, client: client, clientState: clientState, married: married, now: now, retryCount: 1)
deferred.upon { result in
// This could race to update the cache with multiple token results.
// One racer will win -- that's fine, presumably she has the freshest token.
// If not, that's okay, 'cuz the slightly dated token is still a valid token.
if let token = result.successValue {
let newCache = SyncAuthStateCache(token: token, forKey: married.kSync,
expiresAt: now + 1000 * token.durationInSeconds)
log.debug("Fetched token server token! Token expires at \(newCache.expiresAt).")
self.cache.value = newCache
}
}
return chain(deferred, f: { (token: $0, forKey: married.kSync) })
}
return deferMaybe(result.failureValue!)
}
}
}