Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,16 @@ import SwiftUI
@main
struct OAuthApp: App {

@Environment(\.oauth)
var oauth: OAuth

/// Build the scene body
var body: some Scene {

WindowGroup {
ContentView()
}.environmentObject(oauth)
}

WindowGroup(id: "oauth") {
OAWebView()
}.environmentObject(oauth)
}
}
}

Expand All @@ -45,7 +42,7 @@ struct ContentView: View {
@Environment(\.dismissWindow)
private var dismissWindow

@EnvironmentObject
@Environment(\.oauth)
var oauth: OAuth

var body: some View {
Expand All @@ -63,8 +60,8 @@ struct ContentView: View {
}
}
}
.onReceive(oauth.$state) { state in
handle(state: state)
.onChange(of: oauth.state) {
Comment thread
codefiesta marked this conversation as resolved.
handle(state: oauth.state)
}
}

Expand Down Expand Up @@ -96,7 +93,7 @@ struct ContentView: View {
By default, the easiest way to configure OAuthKit is to simply drop an `oauth.json` file into your main bundle and it will get automatically loaded into your swift application and available as an [EnvironmentObject](https://developer.apple.com/documentation/swiftui/environmentobject). You can find an example `oauth.json` file [here](https://github.com/codefiesta/OAuthKit/blob/main/Tests/OAuthKitTests/Resources/oauth.json).

```swift
@EnvironmentObject
@Environment(\.oauth)
var oauth: OAuth
```

Expand All @@ -111,7 +108,7 @@ If you are building your OAuth Providers programatically (recommended for produc

```swift
let providers: [OAuth.Provider] = ...
let options: [OAuth.Option: Any] = [.applicationTag: "com.bundle.identifier"]
let options: [OAuth.Option: Sendable] = [.applicationTag: "com.bundle.identifier"]
let oauth: OAuth = .init(providers: providers, options: options)
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public extension EnvironmentValues {
}
}

struct OAuthKey: EnvironmentKey {
struct OAuthKey: @preconcurrency EnvironmentKey {

/// The default OAuth instance that is loaded into the environment.
static let defaultValue: OAuth = .init(.main)
@MainActor static let defaultValue: OAuth = .init(.main)
}
1 change: 1 addition & 0 deletions Sources/OAuthKit/Extensions/URLRequest+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public extension URLRequest {

/// Attempts to set the authorization header using the access token.
/// - Parameter oath: the oauth holder
@MainActor
mutating func addAuthorization(oath: OAuth) {
switch oath.state {
case .authorized(let auth):
Expand Down
47 changes: 26 additions & 21 deletions Sources/OAuthKit/OAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import Combine
import CryptoKit
import Foundation
import Observation

/// The default file name that holds the list of providers.
private let defaultResourceName = "oauth"
Expand All @@ -24,7 +25,9 @@ public enum OAError: Error {

/// Provides an observable OAuth 2.0 implementation.
/// See: https://datatracker.ietf.org/doc/html/rfc6749
public class OAuth: NSObject, ObservableObject, @unchecked Sendable {
@MainActor
@Observable
public final class OAuth: NSObject {

/// Keys and values used to specify loading or runtime options.
public struct Option: Hashable, Equatable, RawRepresentable, Sendable {
Expand Down Expand Up @@ -193,31 +196,35 @@ public class OAuth: NSObject, ObservableObject, @unchecked Sendable {
}

/// A published list of available OAuth providers to choose from.
@Published
public var providers = [Provider]()

/// An observable published oauth state.
@Published
public var state: State = .empty

/// The url session to use for communicating with providers.
@ObservationIgnored
private lazy var urlSession: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: URLSessionConfiguration.default)
}()

@ObservationIgnored
private var tasks = [Task<(), any Error>]()
private var options: [Option: Any]?
@ObservationIgnored
private var options: [Option: Sendable]?
@ObservationIgnored
private let networkMonitor = NetworkMonitor()
@ObservationIgnored
private var keychain: Keychain = .default

/// Combine subscribers.
@ObservationIgnored
private var subscribers = Set<AnyCancellable>()

/// Initializes the OAuth service with the specified providers.
/// - Parameters:
/// - providers: the list of oauth providers
public init(providers: [Provider] = [Provider](), options: [Option: Any]? = nil) {
public init(providers: [Provider] = [Provider](), options: [Option: Sendable]? = nil) {
super.init()
self.options = options
self.providers = providers
Expand All @@ -230,7 +237,7 @@ public class OAuth: NSObject, ObservableObject, @unchecked Sendable {
/// - Parameters:
/// - bundle: the bundle to load the oauth provider configuration information from.
/// - options: the initialization options to apply
public init(_ bundle: Bundle, options: [Option: Any]? = nil) {
public init(_ bundle: Bundle, options: [Option: Sendable]? = nil) {
super.init()
self.options = options
self.providers = loadProviders(bundle)
Expand Down Expand Up @@ -268,8 +275,7 @@ public class OAuth: NSObject, ObservableObject, @unchecked Sendable {
private func restore() async {
for provider in providers {
if let authorization: OAuth.Authorization = try? keychain.get(key: provider.id), !authorization.isExpired {
await publish(state: .authorized(authorization))
break
publish(state: .authorized(authorization))
}
}
}
Expand Down Expand Up @@ -300,9 +306,9 @@ public extension OAuth {
@discardableResult
func requestAccessToken(provider: Provider, code: String) async -> Result<Token, OAError> {
// Publish the state
await publish(state: .requestingAccessToken(provider))
publish(state: .requestingAccessToken(provider))
guard var urlComponents = URLComponents(string: provider.accessTokenURL.absoluteString) else {
await publish(state: .empty)
publish(state: .empty)
return .failure(.malformedURL)
}
var queryItems = [URLQueryItem]()
Expand All @@ -312,32 +318,32 @@ public extension OAuth {
queryItems.append(URLQueryItem(name: "redirect_uri", value: provider.redirectURI))
urlComponents.queryItems = queryItems
guard let url = urlComponents.url else {
await publish(state: .empty)
publish(state: .empty)
return .failure(.malformedURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Accept")
guard let (data, _) = try? await urlSession.data(for: request) else {
await publish(state: .empty)
publish(state: .empty)
return .failure(.badResponse)
}

// Decode the token
let decoder = JSONDecoder()
guard let token = try? decoder.decode(Token.self, from: data) else {
await publish(state: .empty)
publish(state: .empty)
return .failure(.decoding)
}

// Store the authorization
let authorization = Authorization(issuer: provider.id, token: token)
guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else {
await publish(state: .empty)
publish(state: .empty)
return .failure(.keychain)
}

await publish(state: .authorized(authorization))
publish(state: .authorized(authorization))
return .success(token)
}

Expand All @@ -346,7 +352,7 @@ public extension OAuth {
Task {
debugPrint("⚠️ [Clearing oauth state]")
keychain.clear()
await publish(state: .empty)
publish(state: .empty)
}
}

Expand All @@ -370,27 +376,26 @@ public extension OAuth {
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Accept")
guard let (data, _) = try? await urlSession.data(for: request) else {
return await publish(state: .empty)
return publish(state: .empty)
}

// Decode the token
let decoder = JSONDecoder()
guard let token = try? decoder.decode(Token.self, from: data) else {
return await publish(state: .empty)
return publish(state: .empty)
}

// Store the authorization
let authorization = Authorization(issuer: provider.id, token: token)
guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else {
return await publish(state: .empty)
return publish(state: .empty)
}
await publish(state: .authorized(authorization))
publish(state: .authorized(authorization))
}
}

/// Publishes state on the main thread.
/// - Parameter state: the new state information to publish out on the main thread.
@MainActor
private func publish(state: State) {
switch state {
case .authorized(let auth):
Expand Down
2 changes: 1 addition & 1 deletion Sources/OAuthKit/Views/OAWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import WebKit
@MainActor
public struct OAWebView {

@EnvironmentObject
@Environment(\.oauth)
var oauth: OAuth
let view = WKWebView()

Expand Down
4 changes: 2 additions & 2 deletions Tests/OAuthKitTests/OAuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ final class OAuthTests {
/// Tests the init method using a custom bundle.
@Test("Initializing providers")
func whenInitializing() async throws {
let oauth: OAuth = .init(.module)
let providers = oauth.providers
let oauth: OAuth = await .init(.module)
let providers = await oauth.providers
#expect(providers.isNotEmpty)
}

Expand Down