Skip to content

Commit

Permalink
Merge pull request #27 from Tunous/throwing-authenticateUser
Browse files Browse the repository at this point in the history
  • Loading branch information
daneden authored May 22, 2022
2 parents 53e512c + c5dd6c6 commit 9f50f1f
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Demo App/Twift_SwiftUI/Twift_SwiftUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ struct Twift_SwiftUIApp: App {
footer: Text("Use this authentication method for most cases. This test app enables all user scopes by default.")
) {
AsyncButton {
let (user, _) = await Twift.Authentication().authenticateUser(clientId: CLIENT_ID,
let user = try? await Twift.Authentication().authenticateUser(clientId: CLIENT_ID,
redirectUri: URL(string: TWITTER_CALLBACK_URL)!,
scope: Set(OAuth2Scope.allCases))

Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,21 @@ You can authenticate users with `Twift.Authentication().authenticateUser()`:
```swift
var client: Twift?

let (oauthUser, error) = await Twift.Authentication().authenticateUser(
clientId: TWITTER_CLIENT_ID,
redirectUri: URL(string: TWITTER_CALLBACK_URL)!,
scope: Set(OAuth2Scope.allCases)
)

if let oauthUser = oauthUser {
do {
let oauthUser = try await Twift.Authentication().authenticateUser(
clientId: TWITTER_CLIENT_ID,
redirectUri: URL(string: TWITTER_CALLBACK_URL)!,
scope: Set(OAuth2Scope.allCases)
)
client = Twift(.oauth2UserAuth(oauthUser))

// 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
} catch {
print(error.localizedDescription)
}
```

Expand Down
51 changes: 35 additions & 16 deletions Sources/Twift+Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,33 @@ extension Twift.Authentication {
/// - scope: The user access scopes for your authentication. For automatic token refreshing, ensure that `offlineAccess` is included in the scope.
/// - presentationContextProvider: Optional presentation context provider. When not provided, this function will handle the presentation context itself.
/// - Returns: A tuple containing the authenticated user access tokens or any encoutered error.
@_disfavoredOverload
@available(*, deprecated, message: "Use throwing 'authenticateUser' function instead")
public func authenticateUser(clientId: String,
redirectUri: URL,
scope: Set<OAuth2Scope>,
presentationContextProvider: ASWebAuthenticationPresentationContextProviding? = nil
) async -> (OAuth2User?, Error?) {
do {
let oauthUser: OAuth2User = try await authenticateUser(clientId: clientId, redirectUri: redirectUri, scope: scope, presentationContextProvider: presentationContextProvider)
return (oauthUser, nil)
} catch {
return (nil, error)
}
}

/// Authenticates the user using Twitter's OAuth 2.0 PKCE flow.
/// - Parameters:
/// - clientId: The client ID for your Twitter API app
/// - redirectUri: The URI to redirect users to after completing authentication.
/// - scope: The user access scopes for your authentication. For automatic token refreshing, ensure that `offlineAccess` is included in the scope.
/// - presentationContextProvider: Optional presentation context provider. When not provided, this function will handle the presentation context itself.
/// - Returns: The authenticated user access tokens.
public func authenticateUser(clientId: String,
redirectUri: URL,
scope: Set<OAuth2Scope>,
presentationContextProvider: ASWebAuthenticationPresentationContextProviding? = nil
) async throws -> OAuth2User {
let state = UUID().uuidString

let authUrlQueryItems: [URLQueryItem] = [
Expand All @@ -167,39 +189,36 @@ extension Twift.Authentication {
authUrl.path = "/i/oauth2/authorize"
authUrl.queryItems = authUrlQueryItems

let (returnedUrl, error): (URL?, Error?) = await withCheckedContinuation { continuation in
let returnedUrl: URL = try await withCheckedThrowingContinuation { continuation in
guard let authUrl = authUrl.url else {
return continuation.resume(returning: (nil, TwiftError.UnknownError(nil)))
return continuation.resume(throwing: TwiftError.UnknownError(nil))
}

let authSession = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: redirectUri.scheme) { (url, error) in
return continuation.resume(returning: (url, error))
if let error = error {
return continuation.resume(throwing: error)
}
if let url = url {
return continuation.resume(returning: url)
}
return continuation.resume(throwing: TwiftError.UnknownError("There was a problem authenticating the user: no URL was returned from the first authentication step."))
}

authSession.presentationContextProvider = presentationContextProvider ?? self
authSession.start()
}

if let error = error {
print(error.localizedDescription)
return (nil, error)
}

guard let returnedUrl = returnedUrl else {
return (nil, TwiftError.UnknownError("There was a problem authenticating the user: no URL was returned from the first authentication step."))
}

let returnedUrlComponents = URLComponents(string: returnedUrl.absoluteString)

let returnedState = returnedUrlComponents?.queryItems?.first(where: { $0.name == "state" })?.value
guard let returnedState = returnedState,
returnedState == state else {
return (nil, TwiftError.UnknownError("There was a problem authenticating the user: the state values for the first authentication step are not equal."))
throw TwiftError.UnknownError("There was a problem authenticating the user: the state values for the first authentication step are not equal.")
}

let returnedCode = returnedUrlComponents?.queryItems?.first(where: { $0.name == "code" })?.value
guard let returnedCode = returnedCode else {
return (nil, TwiftError.UnknownError("There was a problem authenticating the user: no request token was found in the returned URL."))
throw TwiftError.UnknownError("There was a problem authenticating the user: no request token was found in the returned URL.")
}

var codeRequest = URLRequest(url: URL(string: "https://api.twitter.com/2/oauth2/token")!)
Expand All @@ -223,12 +242,12 @@ extension Twift.Authentication {
var oauth2User = try JSONDecoder().decode(OAuth2User.self, from: data)
oauth2User.clientId = clientId

return (oauth2User, nil)
return oauth2User
} catch {
print(error.localizedDescription)
}

return (nil, TwiftError.UnknownError("Unable to fetch and decode the OAuth 2.0 user context."))
throw TwiftError.UnknownError("Unable to fetch and decode the OAuth 2.0 user context.")
}
}

Expand Down

2 comments on commit 9f50f1f

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View documentation coverage after this change
Filename Coverage
Total 87.33%
Twift+API.swift 100.00%
Twift+Authentication.swift 97.62%
Twift+Blocks.swift 100.00%
Twift+Bookmarks.swift 100.00%
Twift+Errors.swift 81.25%
Twift+Follows.swift 100.00%
Twift+Likes.swift 100.00%
Twift+Lists.swift 100.00%
Twift+Media.swift 92.59%
Twift+Mutes.swift 100.00%
Twift+Retweets.swift 100.00%
Twift+Search.swift 100.00%
Twift+Spaces.swift 90.00%
Twift+Streams.swift 100.00%
Twift+Tweets.swift 84.62%
Twift+Users.swift 100.00%
Twift.swift 100.00%
Types+List.swift 93.33%
Types+Media.swift 83.33%
Types+Place.swift 76.67%
Types+Poll.swift 86.67%
Types+Spaces.swift 65.71%
Types+Stream.swift 75.00%
Types+Tweet.swift 81.01%
Types+User.swift 88.37%
Types.swift 100.00%

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View documentation coverage after this change
Filename Coverage
Total 87.33%
Twift+API.swift 100.00%
Twift+Authentication.swift 97.62%
Twift+Blocks.swift 100.00%
Twift+Bookmarks.swift 100.00%
Twift+Errors.swift 81.25%
Twift+Follows.swift 100.00%
Twift+Likes.swift 100.00%
Twift+Lists.swift 100.00%
Twift+Media.swift 92.59%
Twift+Mutes.swift 100.00%
Twift+Retweets.swift 100.00%
Twift+Search.swift 100.00%
Twift+Spaces.swift 90.00%
Twift+Streams.swift 100.00%
Twift+Tweets.swift 84.62%
Twift+Users.swift 100.00%
Twift.swift 100.00%
Types+List.swift 93.33%
Types+Media.swift 83.33%
Types+Place.swift 76.67%
Types+Poll.swift 86.67%
Types+Spaces.swift 65.71%
Types+Stream.swift 75.00%
Types+Tweet.swift 81.01%
Types+User.swift 88.37%
Types.swift 100.00%

Please sign in to comment.