Skip to content

Commit b596894

Browse files
authored
Automatically refresh application password (#24742)
* Update wordpress-rs * Automatically refresh application password * Make sure the re-authentication flow is only for the current site (#24743)
1 parent 6c1f6cd commit b596894

File tree

6 files changed

+100
-41
lines changed

6 files changed

+100
-41
lines changed

Modules/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ let package = Package(
5454
),
5555
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
5656
// We can't use wordpress-rs branches nor commits here. Only tags work.
57-
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250715"),
57+
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250813"),
5858
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.8.0-alpha.0"),
5959
.package(
6060
url: "https://github.com/Automattic/color-studio",

Sources/WordPressData/Swift/Blog+SelfHosted.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,16 @@ public enum WordPressSite {
226226
return try context.existingObject(with: blogId)
227227
}
228228
}
229+
230+
public func blogId(in coreDataStack: CoreDataStack) -> TaggedManagedObjectID<Blog>? {
231+
switch self {
232+
case let .dotCom(siteId, _):
233+
return coreDataStack.performQuery { context in
234+
guard let blog = try? Blog.lookup(withID: siteId, in: context) else { return nil }
235+
return TaggedManagedObjectID(blog)
236+
}
237+
case let .selfHosted(id, _, _, _):
238+
return id
239+
}
240+
}
229241
}

WordPress/Classes/Networking/WordPressClient.swift

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ extension WordPressClient {
2222
// rather than using the shared one on disk).
2323
let session = URLSession(configuration: .ephemeral)
2424

25-
let notifier = AppNotifier()
25+
let notifier = AppNotifier(site: site, coreDataStack: ContextManager.shared)
2626
let provider = WpAuthenticationProvider.dynamic(
2727
dynamicAuthenticationProvider: AutoUpdateAuthenticationProvider(site: site, coreDataStack: ContextManager.shared)
2828
)
@@ -42,7 +42,6 @@ extension WordPressClient {
4242
authenticationProvider: provider,
4343
appNotifier: notifier
4444
)
45-
notifier.api = api
4645
self.init(api: api, rootUrl: apiRootURL)
4746
}
4847

@@ -84,31 +83,19 @@ private final class AutoUpdateAuthenticationProvider: @unchecked Sendable, WpDyn
8483
}
8584
}
8685

87-
func update() {
86+
@discardableResult
87+
func update() -> WpAuthentication {
88+
// This line does not require `self.lock`. Putting it behind the `self.lock` may lead to dead lock, because
89+
// `coreDataStack.performQuery` also aquire locks.
90+
let authentication = coreDataStack.performQuery(site.authentication(in:))
91+
8892
self.lock.lock()
8993
defer {
9094
self.lock.unlock()
9195
}
9296

93-
self.authentication = coreDataStack.performQuery { [site] context in
94-
switch site {
95-
case let .dotCom(siteId, _):
96-
guard let blog = try? Blog.lookup(withID: siteId, in: context),
97-
let token = blog.authToken else {
98-
return WpAuthentication.none
99-
}
100-
return WpAuthentication.bearer(token: token)
101-
case let .selfHosted(blogId, _, _, _):
102-
guard let blog = try? context.existingObject(with: blogId),
103-
let username = try? blog.getUsername(),
104-
let password = try? blog.getApplicationToken()
105-
else {
106-
return WpAuthentication.none
107-
}
108-
109-
return WpAuthentication(username: username, password: password)
110-
}
111-
}
97+
self.authentication = authentication
98+
return authentication
11299
}
113100

114101
func auth() -> WordPressAPIInternal.WpAuthentication {
@@ -119,12 +106,57 @@ private final class AutoUpdateAuthenticationProvider: @unchecked Sendable, WpDyn
119106

120107
return self.authentication
121108
}
109+
110+
func refresh() async -> Bool {
111+
guard let blogId = site.blogId(in: coreDataStack) else { return false }
112+
113+
do {
114+
DDLogInfo("Create a new application password")
115+
try await ApplicationPasswordRepository.shared.createPasswordIfNeeded(for: blogId)
116+
} catch {
117+
DDLogInfo("Failed to create a new application password: \(error)")
118+
return false
119+
}
120+
121+
let current = auth()
122+
let newAuth = update()
123+
return newAuth != .none && newAuth != current
124+
}
122125
}
123126

124127
private class AppNotifier: @unchecked Sendable, WpAppNotifier {
125-
weak var api: WordPressAPI?
128+
let site: WordPressSite
129+
let coreDataStack: CoreDataStack
130+
131+
init(site: WordPressSite, coreDataStack: CoreDataStack) {
132+
self.site = site
133+
self.coreDataStack = coreDataStack
134+
}
126135

127136
func requestedWithInvalidAuthentication() async {
128-
NotificationCenter.default.post(name: WordPressClient.requestedWithInvalidAuthenticationNotification, object: api)
137+
let blogId = site.blogId(in: coreDataStack)
138+
NotificationCenter.default.post(name: WordPressClient.requestedWithInvalidAuthenticationNotification, object: blogId)
139+
}
140+
}
141+
142+
private extension WordPressSite {
143+
func authentication(in context: NSManagedObjectContext) -> WpAuthentication {
144+
switch self {
145+
case let .dotCom(siteId, _):
146+
guard let blog = try? Blog.lookup(withID: siteId, in: context),
147+
let token = blog.authToken else {
148+
return WpAuthentication.none
149+
}
150+
return WpAuthentication.bearer(token: token)
151+
case let .selfHosted(blogId, _, _, _):
152+
guard let blog = try? context.existingObject(with: blogId),
153+
let username = try? blog.getUsername(),
154+
let password = try? blog.getApplicationToken()
155+
else {
156+
return WpAuthentication.none
157+
}
158+
159+
return WpAuthentication(username: username, password: password)
160+
}
129161
}
130162
}

WordPress/Classes/Services/ApplicationPasswordRepository.swift

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,15 @@ actor ApplicationPasswordRepository {
5757
return
5858
}
5959

60+
let alreadyStored = await storage
61+
.passwords(belongTo: owners)
62+
.contains { $0.password == authToken }
63+
guard !alreadyStored else { return }
64+
65+
// No need to propagate the API request error.
6066
let api = WordPressAPI(urlSession: URLSession(configuration: .ephemeral), apiRootUrl: apiRootURL, authentication: .init(username: username, password: authToken))
61-
let uuid = try await api.applicationPasswords.retrieveCurrentWithViewContext().data.uuid.uuid
67+
guard let uuid = try? await api.applicationPasswords.retrieveCurrentWithViewContext().data.uuid.uuid else { return }
68+
6269
try await storage.save(.init(password: .init(uuid: uuid, password: authToken), owners: owners))
6370
}
6471

@@ -139,14 +146,7 @@ private extension ApplicationPasswordRepository {
139146
blog.getUrlString(),
140147
)
141148
}
142-
let passwords = await storage.getAll()
143-
.filter {
144-
// This nested loop should not have too much negative impact on performance, since `owners` is a short list (2 elements).
145-
$0.owners.contains { owners.contains($0) }
146-
}
147-
.map {
148-
$0.password
149-
}
149+
let passwords = await storage.passwords(belongTo: owners)
150150

151151
let apiRootURL = try await updateRestAPIURLIfNeeded(blogId)
152152
let siteUsername = try await updateSiteUsernameIfNeeded(blogId)
@@ -407,6 +407,15 @@ private actor ApplicationPasswordStorage {
407407
}
408408
}
409409

410+
extension ApplicationPasswordStorage {
411+
func passwords(belongTo owners: [ApplicationPasswordOwner]) -> [ApplicationPassword] {
412+
getAll()
413+
// This nested loop should not have too much negative impact on performance, since `owners` is a short list (2 elements).
414+
.filter { $0.owners.contains { owners.contains($0) } }
415+
.map { $0.password }
416+
}
417+
}
418+
410419
enum ApplicationPasswordRepositoryError: LocalizedError {
411420
case usernameNotFound
412421
case restApiInaccessible

WordPress/Classes/ViewRelated/NUX/Helpers/WordPressAuthenticationManager.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,13 @@ extension WordPressAuthenticationManager {
9292

9393
notificationCenter.publisher(for: WordPressClient.requestedWithInvalidAuthenticationNotification)
9494
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
95-
.sink { _ in
96-
WordPressAuthenticationManager.showSigninForSelfHostedSiteFixingApplicationPassword()
95+
.sink {
96+
guard let blogId = $0.object as? TaggedManagedObjectID<Blog> else {
97+
wpAssertionFailure("No blog ID found in the requestedWithInvalidAuthenticationNotification notification")
98+
return
99+
}
100+
101+
WordPressAuthenticationManager.showSigninForSelfHostedSiteFixingApplicationPassword(blogId: blogId)
97102
}
98103
.store(in: &cancellables)
99104
}
@@ -301,14 +306,15 @@ extension WordPressAuthenticationManager {
301306
}
302307
}
303308

304-
static func showSigninForSelfHostedSiteFixingApplicationPassword(showNotice: Bool = true) {
309+
static func showSigninForSelfHostedSiteFixingApplicationPassword(blogId: TaggedManagedObjectID<Blog>, showNotice: Bool = true) {
305310
guard let presenter = UIViewController.topViewController,
306311
!presenter.isApplicationReauthentication else {
307312
assertionFailure()
308313
return
309314
}
310315

311-
guard let currentBlog = RootViewCoordinator.sharedPresenter.currentlyVisibleBlog() else {
316+
guard let currentBlog = RootViewCoordinator.sharedPresenter.currentlyVisibleBlog(), currentBlog.objectID == blogId.objectID else {
317+
DDLogWarn("Requested sign in to a site that is not the currently visible one")
312318
return
313319
}
314320

0 commit comments

Comments
 (0)