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
6 changes: 6 additions & 0 deletions Sources/OAuthKit/OAuth+State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,11 @@ extension OAuth {
/// - Provider: the oauth provider
/// - Authorization: the oauth authorization
case authorized(Provider, Authorization)

/// An error has occurred during an authorization flow for the specified provider.
/// - Parameters:
/// - Provider: the oauth provider
/// - OAError: the error information
case error(Provider, OAError)
}
}
53 changes: 18 additions & 35 deletions Sources/OAuthKit/OAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,7 @@ public extension OAuth {
/// - pkce: the pkce data
func token(provider: Provider, code: String, pkce: PKCE? = nil) {
Task(priority: .high) {
let result = await requestToken(provider: provider, code: code, pkce: pkce)
switch result {
case .success(let token):
if provider.debug {
debugPrint("➡️ [Received token], [\(token)]")
}
case .failure(let error):
if provider.debug {
debugPrint("➡️ [Error requesting access token], [\(error)]")
}
}
await requestToken(provider: provider, code: code, pkce: pkce)
}
}

Expand Down Expand Up @@ -280,7 +270,7 @@ private extension OAuth {
schedule(provider: provider, auth: auth)
case .receivedDeviceCode(let provider, let deviceCode):
schedule(provider: provider, deviceCode: deviceCode)
case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode:
case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode:
break
}
self.state = state
Expand Down Expand Up @@ -338,20 +328,16 @@ extension OAuth {
/// - provider: the provider the access token is being requested from
/// - code: the code to exchange
/// - pkce: the PKCE data to pass along with the request
/// - Returns: the exchange result
@discardableResult
func requestToken(provider: Provider, code: String, pkce: PKCE? = nil) async -> Result<Token, OAError> {
func requestToken(provider: Provider, code: String, pkce: PKCE? = nil) async {
// Publish the state
publish(state: .requestingAccessToken(provider))

guard let request = Request.token(provider: provider, code: code, pkce: pkce) else {
publish(state: .empty)
return .failure(.malformedURL)
return publish(state: .error(provider, .malformedURL))
}

guard let (data, response) = try? await urlSession.data(for: request) else {
publish(state: .empty)
return .failure(.badResponse)
return publish(state: .error(provider, .badResponse))
}

if provider.debug {
Expand All @@ -362,19 +348,16 @@ extension OAuth {

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

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

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

/// Refreshes the token for the specified provider.
Expand All @@ -392,7 +375,7 @@ extension OAuth {
}

guard let (data, response) = try? await urlSession.data(for: request) else {
return publish(state: .empty)
return publish(state: .error(provider, .badResponse))
}

if provider.debug {
Expand All @@ -403,13 +386,13 @@ extension OAuth {

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

// Store the authorization
let authorization = Authorization(issuer: provider.id, token: token)
guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else {
return publish(state: .empty)
return publish(state: .error(provider, .keychain))
}
publish(state: .authorized(provider, authorization))
}
Expand All @@ -420,7 +403,7 @@ extension OAuth {
func requestDeviceCode(provider: Provider) async {
guard let request = Request.device(provider: provider) else { return }
guard let (data, response) = try? await urlSession.data(for: request) else {
return publish(state: .empty)
return publish(state: .error(provider, .badResponse))
}

if provider.debug {
Expand All @@ -431,7 +414,7 @@ extension OAuth {

// Decode the device code
guard let deviceCode = try? decoder.decode(DeviceCode.self, from: data) else {
return publish(state: .empty)
return publish(state: .error(provider, .decoding))
}

// Publish the state
Expand All @@ -444,7 +427,7 @@ extension OAuth {
func requestClientCredentials(provider: Provider) async {
guard let request = Request.token(provider: provider) else { return }
guard let (data, response) = try? await urlSession.data(for: request) else {
return publish(state: .empty)
return publish(state: .error(provider, .badResponse))
}

if provider.debug {
Expand All @@ -455,13 +438,13 @@ extension OAuth {

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

// Store the authorization
let authorization = Authorization(issuer: provider.id, token: token)
guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else {
return publish(state: .empty)
return publish(state: .error(provider, .keychain))
}
publish(state: .authorized(provider, authorization))
}
Expand All @@ -474,11 +457,11 @@ extension OAuth {
func poll(provider: Provider, deviceCode: DeviceCode) async {

guard !deviceCode.isExpired, let request = Request.token(provider: provider, deviceCode: deviceCode) else {
return publish(state: .empty)
return publish(state: .error(provider, .malformedURL))
}

guard let (data, response) = try? await urlSession.data(for: request) else {
return publish(state: .empty)
return publish(state: .error(provider, .badResponse))
}

if provider.debug {
Expand All @@ -496,7 +479,7 @@ extension OAuth {
// Store the authorization
let authorization = Authorization(issuer: provider.id, token: token)
guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else {
return publish(state: .empty)
return publish(state: .error(provider, .keychain))
}
publish(state: .authorized(provider, authorization))
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/OAuthKit/OAuthKit.docc/Extensions/State.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ struct ContentView: View {
.foregroundStyle(.blue)
Text("and enter the following code:")
Text(deviceCode.userCode)
case .error(let provider, let error):
Text("Error [\(provider.id)]: \(error.localizedDescription)")
}
}
.onChange(of: oauth.state) { oldState, newState in
Expand Down
8 changes: 5 additions & 3 deletions Sources/OAuthKit/OAuthKit.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ struct ContentView: View {
.padding()
.border(Color.primary)
.font(.title)
case .error(let provider, let error):
Text("Error [\(provider.id)]: \(error.localizedDescription)")
}
}
.onChange(of: oauth.state) { _, state in
Expand Down Expand Up @@ -118,7 +120,7 @@ struct ContentView: View {
private func handle(state: OAuth.State) {
#if canImport(WebKit)
switch state {
case .empty, .requestingAccessToken, .requestingDeviceCode:
case .empty, .error, .requestingAccessToken, .requestingDeviceCode:
break
case .authorizing, .receivedDeviceCode:
openWindow(id: "oauth")
Expand All @@ -140,7 +142,7 @@ OAuthKit provides an out of the box SwiftUI view ``OAWebView`` that will automat
/// - Parameter state: the published state change
func handle(state: OAuth.State) {
switch state {
case .empty, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode:
case .empty, .error, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode:
break
case .authorizing:
openWindow(id: "oauth")
Expand All @@ -165,7 +167,7 @@ struct ContentView: View {
var body: some View {
VStack {
switch oauth.state {
case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode:
case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode:
EmptyView()
case .authorized:
Text("Authorized [\(provider.id)]")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ struct ContentView: View {
.padding()
.border(Color.primary)
.font(.title)
case .error(let provider, let error):
Text("Error [\(provider.id)]: \(error.localizedDescription)")
}
}
.onChange(of: oauth.state) { _, state in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ struct ContentView: View {
.padding()
.border(Color.primary)
.font(.title)
case .error(let provider, let error):
Text("Error [\(provider.id)]: \(error.localizedDescription)")
}
}
.onChange(of: oauth.state) { _, state in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct ContentView: View {
private func handle(state: OAuth.State) {
#if canImport(WebKit)
switch state {
case .empty, .requestingAccessToken, .requestingDeviceCode:
case .empty, .error, .requestingAccessToken, .requestingDeviceCode:
break
case .authorizing, .receivedDeviceCode:
openWindow(id: "oauth")
Expand Down
4 changes: 2 additions & 2 deletions Sources/OAuthKit/Views/OAWebViewCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public class OAWebViewCoordinator: NSObject {
/// - Parameter state: the published state change.
func update(state: OAuth.State) {
switch state {
case .empty, .authorized, .requestingAccessToken, .requestingDeviceCode:
case .empty, .error, .authorized, .requestingAccessToken, .requestingDeviceCode:
break
case .authorizing(let provider, let grantType):
// Override the custom user agent for the provider and tell the browser to load the request
Expand All @@ -99,7 +99,7 @@ extension OAWebViewCoordinator: WKNavigationDelegate {
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
guard let url = navigationAction.request.url else { return .cancel }
switch oauth.state {
case .empty, .requestingAccessToken, .authorized, .requestingDeviceCode, .receivedDeviceCode:
case .empty, .error, .requestingAccessToken, .authorized, .requestingDeviceCode, .receivedDeviceCode:
break
case .authorizing(let provider, let grantType):
handle(url: url, provider: provider, grantType: grantType)
Expand Down
2 changes: 1 addition & 1 deletion Tests/OAuthKitTests/OAWebViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ final class OAWebViewTests {
let monitor: OAuth.Monitor = .init(oauth: oauth)
for await state in monitor.stream {
switch state {
case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode:
case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode:
break
case .authorized(_, _):
keychain.clear()
Expand Down
2 changes: 1 addition & 1 deletion Tests/OAuthKitTests/OAuth+Monitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ extension OAuth {
let state = oauth.state
continuation?.yield(state)
switch state {
case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode:
case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode:
waitForNextValue()
case .authorized(_, _):
continuation?.finish()
Expand Down
26 changes: 13 additions & 13 deletions Tests/OAuthKitTests/OAuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,29 +260,29 @@ final class OAuthTests {

// Token
let oauth: OAuth = .init(.module, options: options)
let result = await oauth.requestToken(provider: provider, code: .secureRandom())
#expect(result == .failure(.decoding))
await oauth.requestToken(provider: provider, code: .secureRandom())
#expect(oauth.state == .error(provider, .decoding))

// Refresh Token
let token: OAuth.Token = .init(accessToken: .secureRandom(), refreshToken: .secureRandom(), expiresIn: 0, scope: "email", type: "Bearer")
let auth: OAuth.Authorization = .init(issuer: provider.id, token: token)
try! oauth.keychain.set(auth, for: provider.id)
await oauth.refreshToken(provider: provider)
#expect(oauth.state == .empty)
#expect(oauth.state == .error(provider, .decoding))
oauth.clear()

// Client Credentials
await oauth.requestClientCredentials(provider: provider)
#expect(oauth.state == .empty)
#expect(oauth.state == .error(provider, .decoding))

// Device Code
await oauth.requestDeviceCode(provider: provider)
#expect(oauth.state == .empty)
#expect(oauth.state == .error(provider, .decoding))

// Device Code Polling
let deviceCode: OAuth.DeviceCode = .init(deviceCode: .secureRandom(), userCode: .secureRandom(), verificationUri: "https://example.com", expiresIn: 200, interval: 0)
await oauth.poll(provider: provider, deviceCode: deviceCode)
#expect(oauth.state == .empty)
#expect(oauth.state == .error(provider, .decoding))
}

@Test("When Received server error")
Expand All @@ -301,29 +301,29 @@ final class OAuthTests {

// Token
let oauth: OAuth = .init(.module, options: options)
let result = await oauth.requestToken(provider: provider, code: .secureRandom())
#expect(result == .failure(.badResponse))
await oauth.requestToken(provider: provider, code: .secureRandom())
#expect(oauth.state == .error(provider, .badResponse))

// Refresh Token
let token: OAuth.Token = .init(accessToken: .secureRandom(), refreshToken: .secureRandom(), expiresIn: 0, scope: "email", type: "Bearer")
let auth: OAuth.Authorization = .init(issuer: provider.id, token: token)
try! oauth.keychain.set(auth, for: provider.id)
await oauth.refreshToken(provider: provider)
#expect(oauth.state == .empty)
#expect(oauth.state == .error(provider, .badResponse))
oauth.clear()

// Client Credentials
await oauth.requestClientCredentials(provider: provider)
#expect(oauth.state == .empty)
#expect(oauth.state == .error(provider, .badResponse))

// Device Code
await oauth.requestDeviceCode(provider: provider)
#expect(oauth.state == .empty)
#expect(oauth.state == .error(provider, .badResponse))

// Device Code Polling
let deviceCode: OAuth.DeviceCode = .init(deviceCode: .secureRandom(), userCode: .secureRandom(), verificationUri: "https://example.com", expiresIn: 200, interval: 0)
await oauth.poll(provider: provider, deviceCode: deviceCode)
#expect(oauth.state == .empty)
#expect(oauth.state == .error(provider, .badResponse))
}

/// Tests the PKCE request parameters.
Expand Down Expand Up @@ -427,7 +427,7 @@ final class OAuthTests {
let monitor: OAuth.Monitor = .init(oauth: oauth)
for await state in monitor.stream {
switch state {
case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode:
case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode:
break
case .authorized(_, _):
oauth.clear()
Expand Down