Skip to content
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

Enable access for token refresh #31

Merged
merged 15 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions .swiftpm/xcode/xcshareddata/xcschemes/Twift.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO"
codeCoverageEnabled = "YES">
shouldUseLaunchSchemeArgsEnv = "NO">
<EnvironmentVariables>
<EnvironmentVariable
key = "ENVIRONMENT"
Expand Down Expand Up @@ -72,13 +71,6 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<EnvironmentVariables>
<EnvironmentVariable
key = "ENVIRONMENT"
value = "PRODUCTION"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ New `Twift` instances must be initiated with either OAuth 2.0 user authenticatio
```swift
// OAuth 2.0 User Authentication
let oauthUser: OAuth2User = OAUTH2_USER
let userAuthenticatedClient = Twift(.oauth2UserAuth(oauthUser: oauthUser)
let userAuthenticatedClient = Twift(oauth2User: oauthUser, onTokenRefresh: saveUserCredentials)

// App-Only Bearer Token
let appOnlyClient = Twift(.appOnly(bearerToken: BEARER_TOKEN)
let appOnlyClient = Twift(appOnlyBearerToken: BEARER_TOKEN)
```

You can authenticate users with `Twift.Authentication().authenticateUser()`:
Expand All @@ -36,12 +36,11 @@ do {
scope: Set(OAuth2Scope.allCases)
)

client = Twift(.oauth2UserAuth(oauthUser))
client = Twift(oauth2User: oauthUser, onTokenRefresh: saveUserCredentials)

// It's recommended that you store your user auth tokens via Keychain or another secure storage method.
// OAuth2User can be encoded to a data object for storage and later retrieval.
let encoded = try? JSONEncoder().encode(oauthUser))
saveUserAuthExample(encoded) // Saves the data to Keychain, for example
saveUserCredentials(oauthUser) // Saves the data to Keychain, for example
} catch {
print(error.localizedDescription)
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Twift+API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension Twift {
body: Data? = nil,
expectedReturnType: T.Type
) async throws -> T {
if case AuthenticationType.oauth2UserAuth(_) = self.authenticationType {
if case AuthenticationType.oauth2UserAuth(_, _) = self.authenticationType {
try await self.refreshOAuth2AccessToken()
}

Expand Down Expand Up @@ -84,7 +84,7 @@ extension Twift {
consumerCredentials: clientCredentials,
userCredentials: userCredentials
)
case .oauth2UserAuth(let oauthUser):
case .oauth2UserAuth(let oauthUser, _):
request.addValue("Bearer \(oauthUser.accessToken)", forHTTPHeaderField: "Authorization")
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Twift+Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension Twift {
/// OAuth 2.0 User Context authentication.
///
/// When this authentication method is used, the `oauth2User` access token may be automatically refreshed by the client if it has expired.
case oauth2UserAuth(_ oauth2User: OAuth2User)
case oauth2UserAuth(_ oauth2User: OAuth2User, onRefresh: ((OAuth2User) -> Void)?)

/// App-only authentication
case appOnly(bearerToken: String)
Expand Down
33 changes: 31 additions & 2 deletions Sources/Twift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import Combine
public class Twift: NSObject, ObservableObject {
/// The type of authentication access for this Twift instance
public private(set) var authenticationType: AuthenticationType
public var oauthUser: OAuth2User? {
switch authenticationType {
case .oauth2UserAuth(let user, _):
return user
default:
return nil
}
}

internal let decoder: JSONDecoder
internal let encoder: JSONEncoder
Expand All @@ -17,6 +25,22 @@ public class Twift: NSObject, ObservableObject {
self.encoder = Self.initializeEncoder()
}

/// Initialises an instance with OAuth2 User authentication
/// - Parameters:
/// - oauth2User: The OAuth2 User object for authenticating requests on behalf of a user
/// - onTokenRefresh: A callback invoked when the access token is refreshed by Twift. Useful for storing updated credentials.
public convenience init(oauth2User: OAuth2User,
onTokenRefresh: @escaping (OAuth2User) -> Void = { _ in }) {
self.init(.oauth2UserAuth(oauth2User, onRefresh: onTokenRefresh))
}

/// Initialises an instance with App-Only Bearer Token authentication
/// - Parameters:
/// - appOnlyBearerToken: The App-Only Bearer Token issued by Twitter for authenticating requests
public convenience init(appOnlyBearerToken: String) {
self.init(.appOnly(bearerToken: appOnlyBearerToken))
}

/// Swift's native implementation of ISO 8601 date decoding defaults to a format that doesn't include milliseconds, causing decoding errors because of Twitter's date format.
/// This function returns a decoder which can decode Twitter's date formats, as well as converting keys from snake_case to camelCase.
static internal func initializeDecoder() -> JSONDecoder {
Expand Down Expand Up @@ -71,9 +95,10 @@ public class Twift: NSObject, ObservableObject {
}

/// Refreshes the OAuth 2.0 token, optionally forcing a refresh even if the token is still valid
/// After a successful refresh, a user-defined callback is performed. (optional)
/// - Parameter onlyIfExpired: Set to false to force the token to refresh even if it hasn't yet expired.
public func refreshOAuth2AccessToken(onlyIfExpired: Bool = true) async throws {
guard case AuthenticationType.oauth2UserAuth(let oauthUser) = self.authenticationType else {
guard case AuthenticationType.oauth2UserAuth(let oauthUser, let refreshCompletion) = self.authenticationType else {
throw TwiftError.WrongAuthenticationType(needs: .oauth2UserAuth)
}

Expand Down Expand Up @@ -106,6 +131,10 @@ public class Twift: NSObject, ObservableObject {
var refreshedOAuthUser = try JSONDecoder().decode(OAuth2User.self, from: data)
refreshedOAuthUser.clientId = clientId

self.authenticationType = .oauth2UserAuth(refreshedOAuthUser)
if let refreshCompletion = refreshCompletion {
refreshCompletion(refreshedOAuthUser)
}

self.authenticationType = .oauth2UserAuth(refreshedOAuthUser, onRefresh: refreshCompletion)
}
}
2 changes: 1 addition & 1 deletion Tests/TwiftTests/TwiftTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import XCTest
@MainActor
final class TwiftTests {
static var userAuthClient: Twift {
Twift(.oauth2UserAuth(OAuth2User(accessToken: "test", refreshToken: "test_refresh", scope: Set(OAuth2Scope.allCases))))
Twift(.oauth2UserAuth(OAuth2User(accessToken: "test", refreshToken: "test_refresh", scope: Set(OAuth2Scope.allCases)), onRefresh: { _ in }))
}

static var appOnlyClient: Twift {
Expand Down