Skip to content

Commit

Permalink
Initial work for OAuth 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
daneden committed Mar 29, 2022
1 parent 01056f8 commit 4d6b0cc
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 13 deletions.
33 changes: 20 additions & 13 deletions Demo App/Twift_SwiftUI/Twift_SwiftUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extension Twift {
switch authenticationType {
case .appOnly(_): return false
case .userAccessTokens(_, _): return true
case .oauth2UserContext(_): return true
}
}
}
Expand Down Expand Up @@ -43,20 +44,26 @@ struct Twift_SwiftUIApp: App {
header: Text("User Access Tokens"),
footer: Text("Use this authentication method for most cases.")
) {
Button {
Twift.Authentication().requestUserCredentials(clientCredentials: clientCredentials, callbackURL: URL(string: TWITTER_CALLBACK_URL)!) { (userCredentials, error) in
if let error = error {
print(error.localizedDescription)
}

if let creds = userCredentials {
DispatchQueue.main.async {
container.client = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: creds))
}
}
}
// Button {
// Twift.Authentication().requestUserCredentials(clientCredentials: clientCredentials, callbackURL: URL(string: TWITTER_CALLBACK_URL)!) { (userCredentials, error) in
// if let error = error {
// print(error.localizedDescription)
// }
//
// if let creds = userCredentials {
// DispatchQueue.main.async {
// container.client = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: creds))
// }
// }
// }
// } label: {
// Text("Sign In With Twitter")
// }

AsyncButton {
let result = await Twift.Authentication().authorizeUser(clientId: "Sm5PSUhRNW9EZ3NXb0tJQkI5WU06MTpjaQ", redirectUri: URL(string: TWITTER_CALLBACK_URL)!, scope: [.offlineAccess, .usersRead, .tweetRead])
} label: {
Text("Sign In With Twitter")
Text("Sign in")
}
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/Twift+API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ extension Twift {
consumerCredentials: clientCredentials,
userCredentials: userCredentials
)
case .oauth2UserContext(let oauthUser):
request.addValue("Bearer \(oauthUser.accessToken)", forHTTPHeaderField: "Authorization")
}
}
}
Expand Down
174 changes: 174 additions & 0 deletions Sources/Twift+Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ extension Twift {
case userAccessTokens(clientCredentials: OAuthCredentials,
userCredentials: OAuthCredentials)


/// 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 oauth2UserContext(oauth2User: OAuth2User)

/// App-only authentication
case appOnly(bearerToken: String)
}
Expand All @@ -23,6 +29,9 @@ extension Twift {
/// A value representing ``AuthenticationType.userAccessTokens(_, _)``
case userAccessTokens

/// A value representing ``AuthenticationType.oauth2UserContext(_)``
case oauth2UserContext

/// A value representing ``AuthenticationType.appOnly(_)``
case appOnly
}
Expand Down Expand Up @@ -123,3 +132,168 @@ extension Twift {
}
}
}

extension Twift.Authentication {
public func authorizeUser(clientId: String,
redirectUri: URL,
scope: Set<OAuth2Scope>,
presentationContextProvider: ASWebAuthenticationPresentationContextProviding? = nil
) async -> (OAuth2User?, Error?) {
let state = UUID().uuidString

let authUrlQueryItems: [URLQueryItem] = [
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "client_id", value: clientId),
URLQueryItem(name: "redirect_uri", value: redirectUri.absoluteString),
URLQueryItem(name: "scope", value: scope.map(\.rawValue).joined(separator: " ")),
URLQueryItem(name: "state", value: state),
URLQueryItem(name: "code_challenge", value: "challenge"),
URLQueryItem(name: "code_challenge_method", value: "plain"),
]

var authUrl = URLComponents()
authUrl.scheme = "https"
authUrl.host = "twitter.com"
authUrl.path = "/i/oauth2/authorize"
authUrl.queryItems = authUrlQueryItems

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

let authSession = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: redirectUri.scheme) { (url, error) in
return continuation.resume(returning: (url, error))
}

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("No returned OAuth URL"))
}

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("Bad state returned from OAuth flow"))
}

let returnedCode = returnedUrlComponents?.queryItems?.first(where: { $0.name == "code" })?.value
guard let returnedCode = returnedCode else {
return (nil, TwiftError.UnknownError("No code returned"))
}

var codeRequest = URLRequest(url: URL(string: "https://api.twitter.com/2/oauth2/token")!)
let body = [
"code": returnedCode,
"grant_type": "authorization_code",
"client_id": clientId,
"redirect_uri": redirectUri.absoluteString,
"code_verifier": "challenge"
]

let encodedBody = try? JSONSerialization.data(withJSONObject: body)

codeRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
codeRequest.httpMethod = "POST"
codeRequest.httpBody = encodedBody

do {
let (data, _) = try await URLSession.shared.data(for: codeRequest)

print(String(data: data, encoding: .utf8))
} catch {
print(error.localizedDescription)
}

return (nil, nil)
}
}

public struct OAuth2User {
public var clientId: String
public var userId: String?
public var accessToken: String
public var refreshToken: String?
public var expiresAt: Date
public var scope: [OAuth2Scope]

public var expired: Bool {
expiresAt < .now
}
}

public enum OAuth2Scope: String, CaseIterable, RawRepresentable {
/// All the Tweets you can view, including Tweets from protected accounts.
case tweetRead = "tweet.read"

/// Tweet and Retweet for you.
case tweetWrite = "tweet.write"

/// Hide and unhide replies to your Tweets.
case tweetModerateWrite = "tweet.moderate.write"

/// Any account you can view, including protected accounts.
case usersRead = "users.read"

/// People who follow you and people who you follow.
case followsRead = "follows.read"

/// Follow and unfollow people for you.
case followsWrite = "follows.write"

/// Stay connected to your account until you revoke access.
case offlineAccess = "offline.access"

/// All the Spaces you can view.
case spaceRead = "space.read"

/// Accounts you’ve muted.
case muteRead = "mute.read"

/// Mute and unmute accounts for you.
case muteWrite = "mute.write"

/// Tweets you’ve liked and likes you can view.
case likeRead = "like.read"

/// Like and un-like Tweets for you.
case likeWrite = "like.write"

/// Lists, list members, and list followers of lists you’ve created or are a member of, including private lists.
case listRead = "list.read"

/// Create and manage Lists for you.
case listWrite = "list.write"

/// Accounts you’ve blocked.
case blockRead = "block.read"

/// Block and unblock accounts for you.
case blockWrite = "block.write"

/// Get Bookmarked Tweets from an authenticated user.
case bookmarkRead = "bookmark.read"

/// Bookmark and remove Bookmarks from Tweets
case bookmarkWrite = "bookmark.write"

/// All write-permission scopes.
static var allWriteScopes: Set<OAuth2Scope> {
[.likeWrite, .listWrite, .muteWrite, .blockWrite, .tweetWrite, .followsWrite, .bookmarkWrite, .tweetModerateWrite]
}

/// All read-permission scopes.
static var allReadScopes: Set<OAuth2Scope> {
[.likeRead, .listRead, .muteRead, .blockRead, .spaceRead, .tweetRead, .usersRead, .followsRead, .bookmarkRead]
}
}
2 changes: 2 additions & 0 deletions Sources/Twift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class Twift: NSObject, ObservableObject {
return userCredentials.userId
case .appOnly(_):
return nil
case .oauth2UserContext(let user):
return user.userId
}
}

Expand Down

0 comments on commit 4d6b0cc

Please sign in to comment.