Skip to content

Commit 22c4af3

Browse files
author
Daiki Matsudate
committed
Swift concurrency support
1 parent 4cc3dbd commit 22c4af3

File tree

3 files changed

+60
-144
lines changed

3 files changed

+60
-144
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import PackageDescription
44

55
let package = Package(
66
name: "OAuth2Client",
7-
platforms: [.iOS(.v14), .macOS(.v11)],
7+
platforms: [.iOS("15.0"), .macOS("12.0")],
88
products: [
99
.library(
1010
name: "OAuth2Client",

README.md

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@ Lightweight OAuth 2.0 Client with PKCE (Proof key for Code Exchange: see [RFC 76
1010
### Sign in
1111

1212
```swift
13-
OAuth2Client().signIn(request: request)
14-
.receive(on: yourQueue)
15-
.sink(receiveCompletion: { (completion) in
16-
17-
}, receiveValue: { (credential) in
18-
credential.save()
19-
})
13+
let credential = await OAuth2Client().signIn(request: request)
2014
```
2115

2216
### Load Access Token
@@ -28,41 +22,33 @@ Credential.load()
2822
### Refresh
2923

3024
```swift
31-
OAuth2Client().refresh(request: request)
32-
.receive(on: yourQueue)
33-
.sink(receiveCompletion: { (completion) in
34-
35-
}, receiveValue: { (credential) in
36-
credential.save()
37-
})
25+
let credential = await OAuth2Client().refresh(request: request)
3826
```
3927

4028
### Sign Out
41-
Removing cache on WebKit, and showing new auth screen.
29+
Removing cache on WebKit, and show new screen to authenticate.
4230

4331
```swift
44-
OAuth2Client().signOut(request: request)
45-
.receive(on: yourQueue)
46-
.sink(receiveCompletion: { (completion) in
47-
48-
}, receiveValue: { (credential) in
49-
credential.save()
50-
})
32+
let credential = await OAuth2Client().signOut(request: request)
5133
```
5234

5335
## Features
5436

5537
- [x] Supporting OAuth 2.0 with PKCE
5638
- [x] Publish / refresh access token
57-
- [x] Combine interface
39+
- [x] Swift Concurrency interface
40+
41+
Note: If you want to use Combine interface, use 0.x version
5842

5943
## General notes for OAuth 2.0
6044
- Make sure setting callback url scheme on your setting
6145

6246
## Supported platforms
6347

64-
- macOS v11.0 and later
65-
- iOS / iPadOS v14.0 and later
48+
- macOS v12.0 or later
49+
- iOS / iPadOS v15.0 or later
50+
51+
Note: After Swift Concurrency is backported, iOS 13.0 / macOS 10.15 can be used
6652

6753
## Installation
6854

@@ -72,6 +58,6 @@ Only support via Swift package manager installation.
7258

7359
```swift
7460
dependencies: [
75-
.package(url: "https://github.com/d-date/OAuth2Client.git", from: "0.1.0")
61+
.package(url: "https://github.com/d-date/OAuth2Client.git", .branch("concurrency"))
7662
]
7763
```
Lines changed: 47 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,85 @@
11
import AuthenticationServices
2-
import Combine
32
import Foundation
43
import WebKit
54
import os.log
65

7-
public class OAuth2Client: NSObject {
8-
9-
private var cancellables: [AnyCancellable] = []
6+
public actor OAuth2Client: NSObject {
107

118
var logger: Logger
129

1310
public init(logger: Logger = .init()) {
1411
self.logger = logger
1512
}
1613

17-
public func signIn(with request: Request) -> Future<Credential, OAuth2Error> {
18-
return Future { [weak self, logger] completion in
19-
guard let self = self else { return }
20-
guard let components = URLComponents(string: request.redirectUri),
21-
let callbackScheme = components.scheme
22-
else {
23-
completion(.failure(OAuth2Error.invalidRedirectUri))
24-
return
25-
}
26-
let pkce = PKCE()
27-
self.requestAuth(url: request.buildAuthorizeURL(pkce: pkce), callbackScheme: callbackScheme)
28-
.flatMap { return self.requestToken(for: request.buildTokenURL(code: $0, pkce: pkce)) }
29-
.sink { (result) in
30-
switch result {
31-
case .failure(let error):
32-
logger.error("\(error.localizedDescription)")
33-
completion(.failure(error))
34-
default: break
35-
}
36-
} receiveValue: { [logger] credential in
37-
logger.debug("\(credential.accessToken)")
38-
completion(.success(credential))
39-
}
40-
.store(in: &self.cancellables)
41-
14+
public func signIn(with request: Request) async throws -> Credential {
15+
guard let components = URLComponents(string: request.redirectUri),
16+
let callbackScheme = components.scheme
17+
else {
18+
throw OAuth2Error.invalidRedirectUri
4219
}
20+
let pkce = PKCE()
21+
let code = try await requestAuth(url: request.buildAuthorizeURL(pkce: pkce), callbackScheme: callbackScheme)
22+
let url = request.buildTokenURL(code: code, pkce: pkce)
23+
return try await requestToken(for: url)
4324
}
4425

45-
public func signOut(with request: Request) -> Future<Credential, OAuth2Error> {
46-
Future { finalCompletion in
47-
let dataTypes = Set([
48-
WKWebsiteDataTypeCookies, WKWebsiteDataTypeSessionStorage, WKWebsiteDataTypeLocalStorage,
49-
WKWebsiteDataTypeWebSQLDatabases, WKWebsiteDataTypeIndexedDBDatabases,
50-
])
51-
Future<Void, Never> { completion in
52-
WKWebsiteDataStore.default().removeData(ofTypes: dataTypes, modifiedSince: Date.distantPast)
53-
{
54-
completion(.success(()))
55-
}
56-
}.flatMap {
57-
self.signIn(with: request)
58-
}
59-
.sink { (completion) in
60-
switch completion {
61-
case let .failure(error):
62-
finalCompletion(.failure(error))
63-
default:
64-
break
65-
}
66-
} receiveValue: { (value) in
67-
finalCompletion(.success(value))
68-
}
69-
.store(in: &self.cancellables)
70-
}
26+
public func signOut(with request: Request) async throws -> Credential {
27+
let dataTypes = Set([
28+
WKWebsiteDataTypeCookies, WKWebsiteDataTypeSessionStorage, WKWebsiteDataTypeLocalStorage,
29+
WKWebsiteDataTypeWebSQLDatabases, WKWebsiteDataTypeIndexedDBDatabases,
30+
])
31+
await WKWebsiteDataStore.default().removeData(ofTypes: dataTypes, modifiedSince: Date.distantPast)
32+
return try await signIn(with: request)
7133
}
7234

73-
public func refresh(with request: Request, refreshToken: String) -> Future<
74-
Credential, OAuth2Error
75-
> {
76-
Future { [weak self] completion in
77-
guard let self = self else { return }
78-
self.requestToken(for: request.buildRefreshTokenURL(refreshToken: refreshToken))
79-
.sink { (result) in
80-
switch result {
81-
case .failure(let error):
82-
completion(.failure(error))
83-
default: break
84-
}
85-
} receiveValue: { credential in
86-
credential.save()
87-
completion(.success(credential))
88-
}
89-
.store(in: &self.cancellables)
90-
}
35+
public func refresh(with request: Request, refreshToken: String) async throws -> Credential {
36+
try await requestToken(for: request.buildRefreshTokenURL(refreshToken: refreshToken))
9137
}
9238
}
9339

9440
extension OAuth2Client: ASWebAuthenticationPresentationContextProviding {
95-
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
41+
nonisolated public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
9642
.init()
9743
}
9844
}
9945

10046
extension OAuth2Client {
101-
fileprivate func requestAuth(url: URL, callbackScheme: String) -> Future<String, OAuth2Error> {
102-
Future { [weak self] finalCompletion in
103-
guard let self = self else { return }
104-
Future<URL, OAuth2Error> { completion in
105-
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackScheme) {
106-
(url, error) in
107-
if let error = error {
108-
completion(.failure(OAuth2Error.authError(error as NSError)))
109-
} else if let url = url {
110-
completion(.success(url))
111-
}
47+
fileprivate func requestAuth(url: URL, callbackScheme: String) async throws -> String {
48+
let url: URL = try await withCheckedThrowingContinuation { continuation in
49+
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackScheme) { url, error in
50+
if let error = error {
51+
continuation.resume(throwing: OAuth2Error.authError(error as NSError))
52+
} else if let url = url {
53+
continuation.resume(returning: url)
11254
}
113-
session.presentationContextProvider = self
114-
session.prefersEphemeralWebBrowserSession = true
115-
session.start()
116-
}.tryMap { url in
117-
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
118-
let code = components.queryItems?.first(where: { $0.name == "code" })?.value
119-
else {
120-
throw OAuth2Error.codeNotFound
121-
}
122-
return code
123-
}.sink { (completion) in
124-
switch completion {
125-
case .failure(let error):
126-
finalCompletion(.failure(OAuth2Error.authError(error as NSError)))
127-
default:
128-
break
129-
}
130-
} receiveValue: { code in
131-
finalCompletion(.success(code))
13255
}
133-
.store(in: &self.cancellables)
56+
session.presentationContextProvider = self
57+
session.prefersEphemeralWebBrowserSession = true
58+
session.start()
59+
}
60+
61+
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
62+
let code = components.queryItems?.first(where: { $0.name == "code" })?.value
63+
else {
64+
throw OAuth2Error.codeNotFound
13465
}
66+
return code
13567
}
13668

137-
fileprivate func requestToken(for url: URL) -> AnyPublisher<Credential, OAuth2Error> {
69+
fileprivate func requestToken(for url: URL) async throws -> Credential {
13870
var request = URLRequest(url: url)
13971
request.httpMethod = "POST"
14072
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
14173
request.addValue("application/json", forHTTPHeaderField: "Accept")
14274

143-
return URLSession.shared.dataTaskPublisher(for: request)
144-
.tryMap { data, response in
145-
guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode
146-
else {
147-
throw OAuth2Error.urlError(URLError(.badServerResponse))
148-
}
149-
return data
150-
}
151-
.decode(type: Credential.self, decoder: JSONDecoder.convertFromSnakeCase)
152-
.mapError { OAuth2Error.decodingError($0 as NSError) }
153-
.eraseToAnyPublisher()
75+
let (data, response) = try await URLSession.shared.data(for: request)
76+
guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
77+
throw OAuth2Error.urlError(URLError(.badServerResponse))
78+
}
79+
do {
80+
return try JSONDecoder.convertFromSnakeCase.decode(Credential.self, from: data)
81+
} catch {
82+
throw OAuth2Error.decodingError(error as NSError)
83+
}
15484
}
15585
}

0 commit comments

Comments
 (0)