Skip to content

Commit

Permalink
Merge pull request #37 from rownd/auth-state
Browse files Browse the repository at this point in the history
Updated logic of AuthenticatorMiddleware
  • Loading branch information
mfmurray committed Jan 6, 2023
2 parents 963614a + bbb4a90 commit 6e5a910
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<key>RowndFrameworkTestApp.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
<key>RowndTests.xcscheme_^#shared#^_</key>
<dict>
Expand Down
58 changes: 33 additions & 25 deletions Sources/Rownd/Models/AppleSignUpCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SwiftUI
import AuthenticationServices
import UIKit
import AnyCodable
import ReSwiftThunk

fileprivate let appleSignInDataKey = "userAppleSignInData"

Expand Down Expand Up @@ -86,31 +87,38 @@ class AppleSignUpCoordinator: NSObject, ASAuthorizationControllerDelegate, ASAut
let idToken = urlContent as String
Auth.fetchToken(idToken: idToken) { authState in
DispatchQueue.main.async {
store.dispatch(SetAuthState(payload: AuthState(accessToken: authState?.accessToken, refreshToken: authState?.refreshToken)))
}
var userData = store.state.user.data

let defaults = UserDefaults.standard
//use UserDefault values for Email and fullName if available
if let userAppleSignInData = defaults.object(forKey: appleSignInDataKey) as? Data {
let decoder = JSONDecoder()
if let loadedAppleSignInData = try? decoder.decode(AppleSignInData.self, from: userAppleSignInData) {
userData["email"] = AnyCodable.init(loadedAppleSignInData.email)
userData["first_name"] = AnyCodable.init(loadedAppleSignInData.firstName)
userData["last_name"] = AnyCodable.init(loadedAppleSignInData.lastName)
userData["full_name"] = AnyCodable.init(loadedAppleSignInData.fullName)
}
} else {
if let email = email {
userData["email"] = AnyCodable.init(email)
userData["first_name"] = AnyCodable.init(fullName?.givenName)
userData["last_name"] = AnyCodable.init(fullName?.familyName)
userData["full_name"] = AnyCodable.init(String("\(fullName?.givenName) \(fullName?.familyName)"))
}
}

DispatchQueue.main.async {
store.dispatch(UserData.save(userData))
store.dispatch(store.state.auth.onReceiveAuthTokens(
AuthState(accessToken: authState?.accessToken, refreshToken: authState?.refreshToken)
))

store.dispatch(Thunk<RowndState> { dispatch, getState in
guard let state = getState() else { return }

var userData = state.user.data

let defaults = UserDefaults.standard
//use UserDefault values for Email and fullName if available
if let userAppleSignInData = defaults.object(forKey: appleSignInDataKey) as? Data {
let decoder = JSONDecoder()
if let loadedAppleSignInData = try? decoder.decode(AppleSignInData.self, from: userAppleSignInData) {
userData["email"] = AnyCodable.init(loadedAppleSignInData.email)
userData["first_name"] = AnyCodable.init(loadedAppleSignInData.firstName)
userData["last_name"] = AnyCodable.init(loadedAppleSignInData.lastName)
userData["full_name"] = AnyCodable.init(loadedAppleSignInData.fullName)
}
} else {
if let email = email {
userData["email"] = AnyCodable.init(email)
userData["first_name"] = AnyCodable.init(fullName?.givenName)
userData["last_name"] = AnyCodable.init(fullName?.familyName)
userData["full_name"] = AnyCodable.init(String("\(fullName?.givenName) \(fullName?.familyName)"))
}
}

if (!userData.isEmpty) {
dispatch(UserData.save(userData))
}
})
}
}
} else {
Expand Down
1 change: 0 additions & 1 deletion Sources/Rownd/Models/Context/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ extension AuthState: Codable {
// This is a special case to get the new auth state over
// to the authenticator as quickly as possible without
// waiting for the store update flow to complete
await Rownd.authenticator.setAuthState(newAuthState)

DispatchQueue.main.async {
dispatch(SetAuthState(payload: newAuthState))
Expand Down
20 changes: 6 additions & 14 deletions Sources/Rownd/framework/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ fileprivate class TokenApiClientDelegate : APIClientDelegate {
// which leads to memmory corruption and weird app crashes.
class AuthenticatorSubscription: NSObject {
private static let inst: AuthenticatorSubscription = AuthenticatorSubscription()
internal static var currentAuthState: AuthState? = store.state.auth

private override init() {}

Expand All @@ -85,13 +86,8 @@ class AuthenticatorSubscription: NSObject {
guard let authState = authState else {
return next(action)
}

Task {
await Rownd.authenticator.setAuthState(authState)
DispatchQueue.main.async {
next(action)
}
}
AuthenticatorSubscription.currentAuthState = authState
next(action)
}
}
}
Expand All @@ -100,19 +96,15 @@ class AuthenticatorSubscription: NSObject {

actor Authenticator {
private let tokenApi = Container.tokenApi()
private var currentAuthState: AuthState? = store.state.auth
private var refreshTask: Task<AuthState, Error>?

func setAuthState(_ newAuthState: AuthState) {
currentAuthState = newAuthState
}

func getValidToken() async throws -> AuthState {
if let handle = refreshTask {
return try await handle.value
}

guard let authState = currentAuthState, let _ = authState.accessToken else {
guard let authState = AuthenticatorSubscription.currentAuthState, let _ = authState.accessToken else {
throw AuthenticationError.noAccessTokenPresent
}

Expand All @@ -136,12 +128,12 @@ actor Authenticator {
Request(
path: "/hub/auth/token",
method: .post,
body: TokenRequest(refreshToken: currentAuthState?.refreshToken)
body: TokenRequest(refreshToken: AuthenticatorSubscription.currentAuthState?.refreshToken)
)
).value

// Store the new token response here for immediate use outside of the state lifecycle
currentAuthState = newAuthState
AuthenticatorSubscription.currentAuthState = newAuthState

// Update the auth state - this really should be abstracted out elsewhere
DispatchQueue.main.async {
Expand Down
47 changes: 16 additions & 31 deletions Tests/RowndTests/AuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,42 +245,27 @@ class AuthTests: XCTestCase {

let accessToken = generateJwt(expires: Date.init(timeIntervalSinceNow: -1000).timeIntervalSince1970) // this will be expired

var subscription: [AnyCancellable] = []
let authSub = store.subscribe { $0.auth }

var numStateUpdateTriggers = 0
authSub
.$current
.removeDuplicates()
.dropFirst()
.sink { authState in
// TODO: This is a rather kludgy way of writing these assertions, but due to the Authenticator middleware, it's the simplest way for the moment. We should find a better flow eventually. It also might not always pass when run with the other tests due to concurrency, etc.
numStateUpdateTriggers += 1
if numStateUpdateTriggers >= 3 {
return
}

if numStateUpdateTriggers == 2 {
XCTAssertFalse(authState.isAuthenticated, "User should no longer be authenticated")
expectation.fulfill()
return
}

XCTAssertTrue(authState.isAuthenticated, "User should be authenticated initially")

Task {
let accessToken = try! await Rownd.getAccessToken()

XCTAssertNil(accessToken, "Returned token should be nil")
}
}
.store(in: &subscription)

store.dispatch(SetAuthState(payload: AuthState(
accessToken: accessToken,
refreshToken: "eyJhbGciOiJFZERTQSIsImtpZCI6InNpZy0xNjQ0OTM3MzYwIn0.eyJqdGkiOiJiNzY4NmUxNC0zYjk2LTQzMTItOWM3ZS1iODdmOTlmYTAxMzIiLCJhdWQiOlsiYXBwOjMzNzA4MDg0OTIyMTU1MDY3MSJdLCJzdWIiOiJnb29nbGUtb2F1dGgyfDExNDg5NTEyMjc5NTQ1MjEyNzI3NiIsImh0dHBzOi8vYXV0aC5yb3duZC5pby9hcHBfdXNlcl9pZCI6ImM5YTgxMDM5LTBjYmMtNDFkNy05YTlkLWVhOWI1YTE5Y2JmMCIsImh0dHBzOi8vYXV0aC5yb3duZC5pby9pc192ZXJpZmllZF91c2VyIjp0cnVlLCJpc3MiOiJodHRwczovL2FwaS5yb3duZC5pbyIsImlhdCI6MTY2NTk3MTk0MiwiaHR0cHM6Ly9hdXRoLnJvd25kLmlvL2p3dF90eXBlIjoicmVmcmVzaF90b2tlbiIsImV4cCI6MTY2ODU2Mzk0Mn0.Yn35j83bfFNgNk26gTvd4a2a2NAGXp7eknvOaFAtd3lWCdvtw6gKRso6Uzd7uydy2MWJFRWC38AkV6lMMfnrDw"
)))

// Dispatch a Thunk to test after the previous state change
store.dispatch(Thunk<RowndState> { dispatch, getState in
guard let state = getState() else { return }

XCTAssertTrue(state.auth.isAuthenticated, "User should be authenticated initially")
Task {
let accessToken = try! await Rownd.getAccessToken()
XCTAssertNil(accessToken, "Returned token should be nil")

guard let state = getState() else { return }
XCTAssertFalse(state.auth.isAuthenticated, "User should no longer be authenticated")

expectation.fulfill()
}
})

waitForExpectations(timeout: 10, handler: nil)
}

Expand Down

0 comments on commit 6e5a910

Please sign in to comment.