Skip to content

Commit

Permalink
Auto-refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurentTreguier committed Aug 26, 2024
1 parent ef0efed commit 63cd56e
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 51 deletions.
16 changes: 16 additions & 0 deletions Fyreplace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
4D13AF7B2C4E8F4200845FDB /* EnvironmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D13AF7A2C4E8F4200845FDB /* EnvironmentPicker.swift */; };
4D13AF812C4E907200845FDB /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D13AF802C4E907200845FDB /* Environment.swift */; };
4D39A4C82BF516B7003FA52E /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 4D39A4C72BF516B7003FA52E /* Localizable.xcstrings */; };
4D4AF71C2C7CE72900621FF3 /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D4AF71B2C7CE72900621FF3 /* Tokens.swift */; };
4D4D394A2C086DA2007196D2 /* PublishedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D4D39492C086DA2007196D2 /* PublishedScreen.swift */; };
4D51F2802C621ADB0018E76E /* ViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D51F27F2C621ADB0018E76E /* ViewProtocol.swift */; };
4D51F2842C621B8D0018E76E /* EventBus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D51F2832C621B8D0018E76E /* EventBus.swift */; };
Expand Down Expand Up @@ -53,7 +54,10 @@
4DC5B1CD2C6FA28E00B75A07 /* RegisterScreenProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC5B1CC2C6FA28E00B75A07 /* RegisterScreenProtocol.swift */; };
4DC5B1CF2C6FA2BE00B75A07 /* MainViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC5B1CE2C6FA2BE00B75A07 /* MainViewProtocol.swift */; };
4DC5B1D62C6FEA2100B75A07 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC5B1D52C6FEA2100B75A07 /* Keychain.swift */; };
4DC5B1D92C720B9800B75A07 /* AuthenticationMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC5B1D82C720B9800B75A07 /* AuthenticationMiddleware.swift */; };
4DC5B1E02C7277B300B75A07 /* AuthenticatingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC5B1DF2C7277B300B75A07 /* AuthenticatingScreen.swift */; };
4DC5B1F42C7A137B00B75A07 /* SettingsScreenProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC5B1F32C7A137B00B75A07 /* SettingsScreenProtocol.swift */; };
4DC5B1F72C7A2F3700B75A07 /* EnvironmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC5B1F62C7A2F3700B75A07 /* EnvironmentView.swift */; };
4DCE062B2C08E5E200F69AF1 /* CompactNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCE062A2C08E5E200F69AF1 /* CompactNavigation.swift */; };
4DCE062D2C08E65300F69AF1 /* RegularNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCE062C2C08E65300F69AF1 /* RegularNavigation.swift */; };
4DE140CD2C52688000A699AE /* DestinationCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DE140CC2C52687F00A699AE /* DestinationCommands.swift */; };
Expand Down Expand Up @@ -90,6 +94,7 @@
4D13AF7A2C4E8F4200845FDB /* EnvironmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentPicker.swift; sourceTree = "<group>"; };
4D13AF802C4E907200845FDB /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
4D39A4C72BF516B7003FA52E /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
4D4AF71B2C7CE72900621FF3 /* Tokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tokens.swift; sourceTree = "<group>"; };
4D4D39492C086DA2007196D2 /* PublishedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedScreen.swift; sourceTree = "<group>"; };
4D51F27F2C621ADB0018E76E /* ViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewProtocol.swift; sourceTree = "<group>"; };
4D51F2832C621B8D0018E76E /* EventBus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBus.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -139,7 +144,10 @@
4DC5B1CC2C6FA28E00B75A07 /* RegisterScreenProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterScreenProtocol.swift; sourceTree = "<group>"; };
4DC5B1CE2C6FA2BE00B75A07 /* MainViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewProtocol.swift; sourceTree = "<group>"; };
4DC5B1D52C6FEA2100B75A07 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
4DC5B1D82C720B9800B75A07 /* AuthenticationMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationMiddleware.swift; sourceTree = "<group>"; };
4DC5B1DF2C7277B300B75A07 /* AuthenticatingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatingScreen.swift; sourceTree = "<group>"; };
4DC5B1F32C7A137B00B75A07 /* SettingsScreenProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenProtocol.swift; sourceTree = "<group>"; };
4DC5B1F62C7A2F3700B75A07 /* EnvironmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentView.swift; sourceTree = "<group>"; };
4DCE062A2C08E5E200F69AF1 /* CompactNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactNavigation.swift; sourceTree = "<group>"; };
4DCE062C2C08E65300F69AF1 /* RegularNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularNavigation.swift; sourceTree = "<group>"; };
4DCE06312C09E19400F69AF1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -184,6 +192,8 @@
children = (
4D13AF802C4E907200845FDB /* Environment.swift */,
4DC5B1D52C6FEA2100B75A07 /* Keychain.swift */,
4DC5B1D82C720B9800B75A07 /* AuthenticationMiddleware.swift */,
4D4AF71B2C7CE72900621FF3 /* Tokens.swift */,
);
path = Data;
sourceTree = "<group>";
Expand All @@ -210,6 +220,7 @@
4D54C96C2BF4E9BE001DE071 /* ArchiveScreen.swift */,
4D54C96E2BF4E9DF001DE071 /* DraftsScreen.swift */,
4D4D39492C086DA2007196D2 /* PublishedScreen.swift */,
4DC5B1F32C7A137B00B75A07 /* SettingsScreenProtocol.swift */,
4D54C9702BF4EA15001DE071 /* SettingsScreen.swift */,
4DC5B1C92C6FA23000B75A07 /* LoginScreenProtocol.swift */,
4D9B3B372C334B3A00A8F7AD /* LoginScreen.swift */,
Expand Down Expand Up @@ -337,6 +348,7 @@
4D5251E82C10532100018CD2 /* Navigation */,
4D4D394B2C086DD3007196D2 /* Screens */,
4D51F27F2C621ADB0018E76E /* ViewProtocol.swift */,
4DC5B1F62C7A2F3700B75A07 /* EnvironmentView.swift */,
4DC5B1CE2C6FA2BE00B75A07 /* MainViewProtocol.swift */,
4D54C92D2BF2608A001DE071 /* MainView.swift */,
);
Expand Down Expand Up @@ -589,6 +601,7 @@
files = (
4D9B3B3D2C34B13E00A8F7AD /* LogoHeader.swift in Sources */,
4DA7BFBB2C5FDEC1005CC4FF /* FakeClient.swift in Sources */,
4D4AF71C2C7CE72900621FF3 /* Tokens.swift in Sources */,
4D54C9712BF4EA15001DE071 /* SettingsScreen.swift in Sources */,
4D13AF7B2C4E8F4200845FDB /* EnvironmentPicker.swift in Sources */,
4DC5B1CF2C6FA2BE00B75A07 /* MainViewProtocol.swift in Sources */,
Expand All @@ -600,8 +613,10 @@
4DE140CD2C52688000A699AE /* DestinationCommands.swift in Sources */,
4D54C96B2BF4E97E001DE071 /* NotificationsScreen.swift in Sources */,
4D51F2802C621ADB0018E76E /* ViewProtocol.swift in Sources */,
4DC5B1D92C720B9800B75A07 /* AuthenticationMiddleware.swift in Sources */,
4D9B3B472C36F50300A8F7AD /* UITextContentType.swift in Sources */,
4D9B3B382C334B3A00A8F7AD /* LoginScreen.swift in Sources */,
4DC5B1F72C7A2F3700B75A07 /* EnvironmentView.swift in Sources */,
4DC5B1E02C7277B300B75A07 /* AuthenticatingScreen.swift in Sources */,
4D5251EF2C1098E900018CD2 /* MultiChoiceScreen.swift in Sources */,
4DB10B502C4FEBFC00634BF6 /* HelpCommands.swift in Sources */,
Expand All @@ -612,6 +627,7 @@
4DCE062B2C08E5E200F69AF1 /* CompactNavigation.swift in Sources */,
4DCE062D2C08E65300F69AF1 /* RegularNavigation.swift in Sources */,
4D54C9692BF4E8F4001DE071 /* FeedScreen.swift in Sources */,
4DC5B1F42C7A137B00B75A07 /* SettingsScreenProtocol.swift in Sources */,
4DC5B1D62C6FEA2100B75A07 /* Keychain.swift in Sources */,
4D9B3B3A2C334B6300A8F7AD /* RegisterScreen.swift in Sources */,
4DB2E36D2C416611007F958D /* SubmitButton.swift in Sources */,
Expand Down
6 changes: 5 additions & 1 deletion Fyreplace/Config/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ struct Config {
}

func client(for environment: ServerEnvironment) -> Client {
return Client(serverURL: url(for: environment), transport: URLSessionTransport())
return Client(
serverURL: url(for: environment),
transport: URLSessionTransport(),
middlewares: [AuthenticationMiddleware()]
)
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions Fyreplace/Config/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
<dict>
<key>App</key>
<dict>
<key>Name</key>
<string>$(PRODUCT_NAME)</string>
<key>Api</key>
<dict>
<key>Dev</key>
Expand All @@ -24,7 +22,13 @@
<key>Website</key>
<string>$(FYREPLACE_WEBSITE)</string>
</dict>
<key>Name</key>
<string>$(PRODUCT_NAME)</string>
</dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.fyreplace.Fyreplace.tokenRefresh</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>Sentry</key>
Expand All @@ -34,6 +38,7 @@
</dict>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
</dict>
Expand Down
24 changes: 24 additions & 0 deletions Fyreplace/Data/AuthenticationMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation
import HTTPTypes
import OpenAPIRuntime

struct AuthenticationMiddleware: ClientMiddleware {
private let keychain = Keychain(service: "connection.token")

func intercept(
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL,
operationID _: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) {
var request = request
let token = keychain.get()

if !token.isEmpty {
request.headerFields[.authorization] = "Bearer \(token)"
}

return try await next(request, body, baseURL)
}
}
11 changes: 7 additions & 4 deletions Fyreplace/Data/Keychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Security
import SwiftUI

struct Keychain {
let service: String
private let service: String

private var query: [CFString: Any] {
[
Expand All @@ -14,6 +14,10 @@ struct Keychain {
]
}

init(service: String) {
self.service = service.replacing(".", with: ":")
}

func get() -> String {
var info = query
info[kSecReturnData] = true
Expand Down Expand Up @@ -72,9 +76,8 @@ struct KeychainStorage: DynamicProperty {
}

init(_ key: String) {
let cleanKey = key.replacing(".", with: ":")
keychain = .init(service: cleanKey)
cache = .shared(for: cleanKey, defaultValue: keychain.get())
keychain = .init(service: key)
cache = .shared(for: key, defaultValue: keychain.get())
}
}

Expand Down
36 changes: 36 additions & 0 deletions Fyreplace/Data/Tokens.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import BackgroundTasks

let tokenRefreshDelaySeconds: Double = 60 * 60 * 24

@available(macOS, unavailable)
func tokenRefreshBackgroundTask() async {
let keychain = Keychain(service: "connection.token")
guard !keychain.get().isEmpty else { return }
scheduleTokenRefresh()

let environment: ServerEnvironment

if let environmentString = UserDefaults.standard.string(forKey: "connection.environment") {
environment = .init(rawValue: environmentString)!
} else {
environment = .default
}

if let newToken = await refreshToken(using: Config.default.app.api.client(for: environment)) {
_ = await keychain.set(newToken)
}
}

@available(macOS, unavailable)
func scheduleTokenRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "app.fyreplace.Fyreplace.tokenRefresh")
request.earliestBeginDate = .init(timeIntervalSinceNow: tokenRefreshDelaySeconds)
try? BGTaskScheduler.shared.submit(request)
}

func refreshToken(using client: APIProtocol) async -> String? {
guard let response = try? await client.getNewToken().ok,
let newToken = try? await String(collecting: response.body.plainText, upTo: 1024)
else { return nil }
return newToken
}
5 changes: 4 additions & 1 deletion Fyreplace/FyreplaceApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ struct FyreplaceApp: App {
let eventBus = EventBus()

WindowGroup {
MainView(eventBus: eventBus)
EnvironmentView(eventBus: eventBus)
}
.commands {
ToolbarCommands()
SidebarCommands()
DestinationCommands(eventBus: eventBus)
HelpCommands()
}
#if !os(macOS)
.backgroundTask(.appRefresh("app.fyreplace.Fyreplace.tokenRefresh"), action: tokenRefreshBackgroundTask)
#endif
}
}
26 changes: 26 additions & 0 deletions Fyreplace/Views/EnvironmentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SwiftUI

struct EnvironmentView: View {
let eventBus: EventBus

#if os(macOS)
@Environment(\.controlActiveState)
private var status
#else
@Environment(\.scenePhase)
private var status
#endif

@Environment(\.config)
private var config

@AppStorage("connection.environment")
private var environment = ServerEnvironment.default

var body: some View {
MainView()
.environment(\.isInForeground, status != .inactive)
.environment(\.api, config.app.api.client(for: environment))
.environmentObject(eventBus)
}
}
43 changes: 24 additions & 19 deletions Fyreplace/Views/MainView.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import SwiftUI

struct MainView: View, MainViewProtocol {
let eventBus: EventBus

@State
var showError = false

Expand All @@ -15,19 +13,14 @@ struct MainView: View, MainViewProtocol {
@State
var failures: [FailureEvent] = []

@Environment(\.config)
private var config
@EnvironmentObject
private var eventBus: EventBus

#if os(macOS)
@Environment(\.controlActiveState)
private var status
#else
@Environment(\.scenePhase)
private var status
#endif
@Environment(\.api)
private var client

@AppStorage("connection.environment")
private var environment = ServerEnvironment.default
@KeychainStorage("connection.token")
private var token

var body: some View {
#if os(macOS)
Expand All @@ -37,9 +30,6 @@ struct MainView: View, MainViewProtocol {
#endif

navigation
.environment(\.isInForeground, status != .inactive)
.environment(\.api, config.app.api.client(for: environment))
.environmentObject(eventBus)
.alert(
isPresented: $showError,
error: errors.first
Expand All @@ -64,13 +54,28 @@ struct MainView: View, MainViewProtocol {
Text(failure.text)
}
)
.onReceive(eventBus.events.compactMap { ($0 as? ErrorEvent)?.error }, perform: addError(_:))
.onReceive(eventBus.events.compactMap { ($0 as? FailureEvent) }, perform: addFailure(_:))
.onReceive(eventBus.events.compactMap { ($0 as? ErrorEvent)?.error }, perform: addError)
.onReceive(eventBus.events.compactMap { ($0 as? FailureEvent) }, perform: addFailure)
#if os(macOS)
.task { await keepRefreshingToken() }
#endif
}

private func keepRefreshingToken() async {
do {
while true {
try await Task.sleep(for: .seconds(tokenRefreshDelaySeconds))

if let newToken = await refreshToken(using: client) {
token = newToken
}
}
} catch {}
}
}

#Preview {
MainView(eventBus: .init())
MainView()
}

struct ForegroundEnvironmentKey: EnvironmentKey {
Expand Down
5 changes: 5 additions & 0 deletions Fyreplace/Views/Screens/LoginScreenProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ extension LoginScreenProtocol {
switch response.body {
case let .plainText(body):
token = try await .init(collecting: body, upTo: 1024)
identifier = ""
randomCode = ""
isWaitingForRandomCode = false

#if !os(macOS)
scheduleTokenRefresh()
#endif
}
case .badRequest:
eventBus.send(.failure(title: "Login.Error.BadRequest.Title", text: "Login.Error.BadRequest.Message"))
Expand Down
Loading

0 comments on commit 63cd56e

Please sign in to comment.