Skip to content

Commit

Permalink
Merge branch 'develop' into fix/cache-key-for-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
KaterinaWire authored Nov 22, 2024
2 parents 5a965ca + 62f70f4 commit 76e4442
Show file tree
Hide file tree
Showing 150 changed files with 2,902 additions and 443 deletions.
4 changes: 3 additions & 1 deletion WireAPI/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ let package = Package(
"WireAPI",
"WireAPISupport",
.product(name: "WireTestingPackage", package: "WireFoundation"),
.product(name: "WireFoundationSupport", package: "WireFoundation"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
],
resources: [
Expand All @@ -47,7 +48,8 @@ let package = Package(
.process("APIs/UserPropertiesAPI/Resources"),
.process("APIs/SelfUserAPI/Resources"),
.process("APIs/UserClientsAPI/Resources"),
.process("Network/PushChannel/Resources")
.process("Network/PushChannel/Resources"),
.process("Authentication/Resources")
]
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ public protocol PushChannelAPI {
/// - Parameter clientID: The id of the self client.
/// - Returns: A push channel.

func createPushChannel(clientID: String) throws -> any PushChannelProtocol
func createPushChannel(clientID: String) async throws -> any PushChannelProtocol

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class PushChannelAPIImpl: PushChannelAPI {
self.pushChannelService = pushChannelService
}

func createPushChannel(clientID: String) throws -> any PushChannelProtocol {
func createPushChannel(clientID: String) async throws -> any PushChannelProtocol {
var components = URLComponents(string: "/await")
components?.queryItems = [URLQueryItem(name: "client", value: clientID)]

Expand All @@ -38,7 +38,7 @@ class PushChannelAPIImpl: PushChannelAPI {
.withMethod(.get)
.build()

return try pushChannelService.createPushChannel(request)
return try await pushChannelService.createPushChannel(request)
}

}
172 changes: 172 additions & 0 deletions WireAPI/Sources/WireAPI/Authentication/AuthenticationManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//
// Wire
// Copyright (C) 2024 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation
import WireFoundation

// sourcery: AutoMockable
protocol AuthenticationManagerProtocol {

func getValidAccessToken() async throws -> AccessToken
func refreshAccessToken() async throws -> AccessToken

}

actor AuthenticationManager: AuthenticationManagerProtocol {

enum Failure: Error, Equatable {

case invalidCredentials

}

private enum CurrentToken {

case cached(AccessToken)
case renewing(Task<AccessToken, any Error>)

}

private var currentToken: CurrentToken?
private let clientID: String
private let cookieStorage: any CookieStorageProtocol
private let networkService: NetworkService

init(
clientID: String,
cookieStorage: any CookieStorageProtocol,
networkService: NetworkService
) {
self.clientID = clientID
self.cookieStorage = cookieStorage
self.networkService = networkService
}

/// Get a valid access token to make authenticated requests.
///
/// If a valid token exists in the cache then it will be returned,
/// otherwise a new token will be retrieved from the backend.
///
/// - Returns: A valid (non-expired) access token.

func getValidAccessToken() async throws -> AccessToken {
switch currentToken {
case let .renewing(task):
// A new token will come soon, wait
try await task.value

case let .cached(accessToken) where !accessToken.isExpiring:
// This one is still good.
accessToken

default:
// Time for a new token.
try await refreshAccessToken()
}
}

/// Get a new access token from the backend.
///
/// This method will fetch a new access token from the backend
/// and then store it in the cache. Only a single request is made
/// at a time, and repeated calls will await the result of any
/// in-flight requests.
///
/// - Returns: A new access token.

func refreshAccessToken() async throws -> AccessToken {
if case let .renewing(task) = currentToken {
// A new token will come soon, wait
return try await task.value
}

var lastKnownToken: AccessToken?
if case let .cached(token) = currentToken {
lastKnownToken = token
}

let task = makeRenewTokenTask(lastKnownToken: lastKnownToken)
currentToken = .renewing(task)

do {
let newToken = try await task.value
currentToken = .cached(newToken)
return newToken
} catch {
currentToken = nil
throw error
}
}

private func makeRenewTokenTask(
lastKnownToken: AccessToken?
) -> Task<AccessToken, any Error> {
Task {
let cookies = try await cookieStorage.fetchCookies()

var request = try URLRequestBuilder(path: "/access")
.withQueryItem(name: "client_id", value: clientID)
.withMethod(.post)
.withAcceptType(.json)
.withCookies(cookies)
.build()

if let lastKnownToken {
request.setAccessToken(lastKnownToken)
}

let (data, response) = try await networkService.executeRequest(request)

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

return try ResponseParser(decoder: decoder)
.success(code: .ok, type: AccessTokenPayload.self)
.failure(code: .forbidden, label: "invalid-credentials", error: Failure.invalidCredentials)
.parse(code: response.statusCode, data: data)
}
}

}

extension AccessToken {

var isExpiring: Bool {
let secondsRemaining = expirationDate.timeIntervalSinceNow
return secondsRemaining < 40
}

}

private struct AccessTokenPayload: Decodable, ToAPIModelConvertible {

let user: UUID
let accessToken: String
let tokenType: String
let expiresIn: Int

func toAPIModel() -> AccessToken {
AccessToken(
userID: user,
token: accessToken,
type: tokenType,
expirationDate: Date(timeIntervalSinceNow: TimeInterval(expiresIn))
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,24 @@ public protocol AuthenticationStorage {
///
/// - Parameter accessToken: The token to store.

func storeAccessToken(_ accessToken: AccessToken)
func storeAccessToken(_ accessToken: AccessToken) async

/// Fetch a stored access token.
///
/// - Returns: The stored access token.

func fetchAccessToken() -> AccessToken?
func fetchAccessToken() async -> AccessToken?

/// Store a cookie.
/// Store cookies.
///
/// - Parameter cookieData: The cookie data to store.
/// - Parameter cookies: The cookies to store.

func storeCookieData(_ cookieData: Data?)
func storeCookies(_ cookies: [HTTPCookie]) async throws

/// Fetch a stored cookie.
/// Fetch stored cookies.
///
/// - Returns: The stored cookie data.
/// - Returns: The stored cookies.

func fetchCookieData() -> Data?
func fetchCookies() async throws -> [HTTPCookie]

}
Loading

0 comments on commit 76e4442

Please sign in to comment.