-
Notifications
You must be signed in to change notification settings - Fork 162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Authentication – Refresh, persistence & API #681
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #681 +/- ##
==========================================
- Coverage 40.92% 40.64% -0.29%
==========================================
Files 525 536 +11
Lines 20880 21647 +767
==========================================
+ Hits 8546 8799 +253
- Misses 12334 12848 +514 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jazak Allah khair, Mohannad!
I believe we need to refine the design of a few components to make them more reusable and minimize the chances of failure. Let me know if you have any questions or need clarification.
Thank you!
Data/AuthenticationClient/Sources/AuthentincationDataManager.swift
Outdated
Show resolved
Hide resolved
Data/AuthenticationClient/Sources/AuthentincationDataManager.swift
Outdated
Show resolved
Hide resolved
public protocol OAuthClient { | ||
/// Expected to be configuered with the host app's OAuth configuration before further operations are attempted. | ||
public protocol AuthentincationDataManager { | ||
/// Sets the app configuration to be used for authentication. | ||
func set(appConfiguration: OAuthAppConfiguration) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed in the previous PR, please move this code to the class initializer. This ensures the class cannot exist without a configuration, making it less prone to invalid states and reducing the number of states the class needs to manage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay then, I moved setting the configurations to the initialization. However, I'm still not comfortable to make it nullable.
As explained in the PR's description: The public state of the client needs to indicate two non-usable states (not-configured and not-authenticate), so I saw that it's better to unify access through a non-optional type for both of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The public state of the client needs to indicate two non-usable states (not-configured and not-authenticate)
Do you mind explaining why is that please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Services using the client need to guard against the state of not being authenticated. They also need to guard against the case of the client not having been configured (if cloned by someone.) These are the two non-usable states of the client.
The client needs to expose the first state, so my first idea is to unify all states of the client (not-configured, non-authenticated & authenticated) through the authenticationState
property. I favoured this over handling one state through nil checking and the other through a property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, a lot of the vagueness here will be cleared once we get to integrating the new APIs along the normal flow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, a client instance should be configured for a consumer to use it. If the consumer does not configure the client, no client will be available for use. A null client serves as a clear indication that the app is not configured for authentication. This approach aligns with best practices followed by many system APIs, where the API is configured during initialization to ensure it can be used effectively.
There are additional nuances to consider, such as handling invalid configurations. Ideally, verifying the configuration during initialization would be great, but I assume this isn't feasible due to the need for backend API requests. Therefore, late verification of the configuration is acceptable. However, when early validation is possible, we should prioritize it and not defer it.
func set(appConfiguration: OAuthAppConfiguration) | ||
|
||
/// Performs the login flow to Quran.com | ||
/// | ||
/// - Parameter viewController: The view controller to be used as base for presenting the login flow. | ||
/// - Returns: Nothing is returned for now. The client may return the profile infromation in the future. | ||
func login(on viewController: UIViewController) async throws | ||
|
||
/// Returns `true` if the client is authenticated. | ||
func restoreState() async throws -> Bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what this method does. Can you explain please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw the implementation below and got what it does. I believe we should reconsider whether this needs to be a public API. Similar to how saving the auth token is treated as an implementation detail, retrieving the auth token from a persistence store should also remain an implementation detail, IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps naming needs to be reconsidered. The purpose of this is to restore and reevaluate the status. The full picture of this API is to be used by sync logic.
But your intuition is on point: this part is still not clear. The main remaining point is synchronization especially on startup, as this API is intended to be the base for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I’ve observed in other auth libraries is that they typically provide functionality to check isAuthenticated
and perform login
, similar to your implementation. In addition to that they often include either an explicit start
method or implicitly initiate the auth process during the initialization of the service/client class. So, that would be my recommendation.
Data/AuthenticationClient/Sources/AuthentincationDataManagerImpl.swift
Outdated
Show resolved
Hide resolved
} | ||
|
||
/// An abstraction for secure persistance of the authentication state. | ||
protocol Persistance { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest creating a general SecurePersistence
API (under Core/ directory) rather than one specifically for authentication.
A recommended pattern can be seen in this directory, where Preferences
is implemented as a service, and consumers define PreferenceKey
objects to describe the data that needs to be persisted. I think we can do something similar with SecurePersistence
and SecurePersistenceKey
.
Also please add a couple of tests for this class.
caller = OAuthCallerMock() | ||
persistance = PersistanceMock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend minimizing the use of mocks, as they can make tests more dependent on the mock behavior than the actual application code. Instead, I recommend focusing on mocking only the third-party API for authentication and avoid creating mocks for the code we own. This approach ensures tests remain closely aligned with the application's real behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually of the opinion that one should avoid mocking 3rd party code. Not often that these would be open for subclassing or easy for subclassing. The class OIDAuthState
of the library we're using proved to be so. It was open, but I'd have to mock four other types. Same issue prevented from stubbing it.
Moreover, wrapping 3rd party code under abstractions allows to define the interactions between the main logic and the kind of responsibility we seek from the 3rd party code. In other words, we're adapting the 3rd party code to our needs - as much as the situation allows without awkward design.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for depending on mocks, I understand that this is a concern, however, I try to counter that by keeping mocks as small as possible, and relying only on providing ready responses (call succeeds or fails, call returns something, ...)
I use the term 'mock' liberally, what I usually use are mostly stubs and spies.
I expounded on that, because these are key techniques I use (dealing with 3rd party and testing doubles strategies.) We can discuss that face-to-face if there's something to be aligned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies, I’m not sure I fully understand your point. You mentioned wanting to avoid mocking but also referred to implementing small mocks.
To clarify my perspective: tests should avoid using mocks/fakes/spies/stubs unless absolutely necessary. These cases include scenarios where the actual dependency cannot be used in tests (e.g., backend APIs or time-based APIs) or where using a mock provides a significant benefit (e.g., file system operations that could slow tests due to extensive reading and writing).
If a mock is needed, it should be added to the following directory: Core/SystemDependencies. Additionally, a corresponding fake implementation should be placed in: Core/SystemDependenciesFake. This approach ensures that we explicitly allowlist the APIs for which their actual implementation cannot be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mentioned wanting to avoid mocking but also referred to implementing small mocks.
I avoid mocking 3rd party code; in our case, that would be keychain access functions and the AppAuth-iOS library. My solution for this would be wrapping such code behind an abstraction and providing a mock that extends that.
tests should avoid using mocks/fakes/spies/stubs unless absolutely necessary
I see. I largely agree. I usually mock things that represent complex logic and I'd think of mocking first for cross-package/cross-layer dependencies. But since this is the practice we're taking here, I'll adjust it.
For our case though, we have:
- Keychain access, wrapped by
Persistence
. I can see this fits well with SystemDependencies. - The OAuth library, wrapped by
OAuthCaller
. Do you that sitting there?
Data/AuthenticationClient/Tests/AuthentincationDataManagerTests.swift
Outdated
Show resolved
Hide resolved
Data/AuthenticationClient/Tests/AuthentincationDataManagerTests.swift
Outdated
Show resolved
Hide resolved
@mohamede1945
There is one or two points that I've put off for now:
|
This PR should complete all the functionalities needed to handle the infrastructure of authentication, bar the logout API. This should be handled when adding the details of the login and profile UI.
Main Changes
Some Design Points