-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathWebAuthentication.swift
383 lines (341 loc) · 16.4 KB
/
WebAuthentication.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
//
// Copyright (c) 2021-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
//
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and limitations under the License.
//
@_exported import AuthFoundation
import Foundation
import OktaOAuth2
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
public enum WebAuthenticationError: Error {
case noCompatibleAuthenticationProviders
case noSignOutFlowProvided
case cannotStartBrowserSession
case cannotComposeAuthenticationURL
case authenticationProvider(error: any Error)
case noAuthenticatorProviderResonse
case serverError(_ error: OAuth2ServerError)
case invalidRedirectScheme(_ scheme: String?)
case userCancelledLogin
case missingIdToken
case oauth2(error: OAuth2Error)
case generic(error: any Error)
case genericError(message: String)
}
/// Authentication coordinator that simplifies signing users in using browser-based OIDC authentication flows.
///
/// This simple class encapsulates the details of managing browser instances across iOS/macOS versions, coordinating with OAuth2 endpoints, and supporting a variety of conveniences when signing users into your application.
///
/// The simplest way to authenticate a user is to use the ``shared`` property to access your default session, and calling ``signIn(from:)`` to present the browser to the user.
///
/// ```swift
/// let token = try await WebAuthentication.shared?.signIn(from: view.window)
/// ```
///
/// To customize the authentication flow, please read more about the underlying OAuth2 client within the OktaOAuth2 library, and how that relates to the ``signInFlow`` or ``signOutFlow`` properties.
///
/// > Important: If your application targets iOS 9.x-10.x, you should add the redirect URI for your client configuration to your app's supported URL schemes. This is because users on devices older than iOS 11 will be prompted to sign in using `SFSafariViewController`, which does not allow your application to detect the final token redirect.
@MainActor
public final class WebAuthentication {
#if os(macOS)
public typealias WindowAnchor = NSWindow
#else
public typealias WindowAnchor = UIWindow
#endif
/// Active / default shared instance of the ``WebAuthentication`` session.
///
/// This convenience property can be used in one of two ways:
///
/// 1. Access a shared instance using the settings configured within a file named `Okta.plist`
/// 2. Programmatically create an instance that can be shared across your application.
///
/// For more information on how to configure your client, see <doc:ConfiguringYourClient> for more details.
public private(set) static var shared: WebAuthentication? {
set {
_shared = newValue
}
get {
guard let result = _shared else {
_shared = try? WebAuthentication()
return _shared
}
return result
}
}
/// The underlying OAuth2 flow that implements the authentication behavior.
public let signInFlow: AuthorizationCodeFlow
/// The underlying OAuth2 flow that implements the session logout behaviour.
public let signOutFlow: SessionLogoutFlow?
/// Indicates whether or not the developer prefers an ephemeral browser session, or if the user's browser state should be shared with the system browser.
public var ephemeralSession: Bool = false
/// Starts sign-in using the configured client.
/// - Parameters:
/// - window: Window from which the sign in process will be started.
/// - context: Context options used when composing the authorization URL.
/// - Returns: The token representing the signed-in user.
@MainActor
public final func signIn(from window: WindowAnchor? = nil,
context: AuthorizationCodeFlow.Context = .init()) async throws -> Token
{
if provider != nil {
cancel()
}
guard let redirectUri = signInFlow.client.configuration.redirectUri
else {
throw OAuth2Error.missingRedirectUri
}
async let authorizeUrl = signInFlow.start(with: context)
guard let provider = try await Self.providerFactory.createWebAuthenticationProvider(
for: self,
from: window,
usesEphemeralSession: ephemeralSession)
else {
throw WebAuthenticationError.noCompatibleAuthenticationProviders
}
self.provider = provider
let url = try await provider.open(authorizeUrl: authorizeUrl,
redirectUri: redirectUri)
self.provider = nil
return try await signInFlow.resume(with: url)
}
/// Starts log-out using the credential.
/// - Parameters:
/// - window: Window from which the sign in process will be started.
/// - credential: Stored credentials that will retrieve the ID token.
/// - context: Context options used when composing the signout URL.
@discardableResult
@MainActor
public final func signOut(from window: WindowAnchor? = nil,
credential: Credential?,
context: SessionLogoutFlow.Context = .init()) async throws -> URL
{
try await signOut(from: window, token: credential?.token, context: context)
}
/// Starts log-out using the `Token` object.
/// - Parameters:
/// - window: Window from which the sign in process will be started.
/// - token: Token object that will retrieve the ID token.
/// - context: Context options used when composing the signout URL.
@discardableResult
@MainActor
public final func signOut(from window: WindowAnchor? = nil,
token: Token?,
context: SessionLogoutFlow.Context = .init()) async throws -> URL
{
var context = context
context.idToken = token?.idToken?.rawValue
return try await signOut(from: window, context: context)
}
/// Starts log-out using the ID token.
/// - Parameters:
/// - window: Window from which the sign in process will be started.
/// - context: Context options used when composing the signout URL.
/// - completion: Completion block that will be invoked when sign-out finishes.
@discardableResult
@MainActor
public final func signOut(from window: WindowAnchor?,
context: SessionLogoutFlow.Context = .init()) async throws -> URL
{
guard let signOutFlow,
let redirectUri = signOutFlow.client.configuration.logoutRedirectUri
else {
throw WebAuthenticationError.noSignOutFlowProvided
}
if provider != nil {
cancel()
}
async let authorizeUrl = signOutFlow.start(with: context)
guard let provider = try await Self.providerFactory.createWebAuthenticationProvider(
for: self,
from: window,
usesEphemeralSession: ephemeralSession)
else {
throw WebAuthenticationError.noCompatibleAuthenticationProviders
}
self.provider = provider
defer { self.provider = nil }
return try await provider.open(authorizeUrl: authorizeUrl,
redirectUri: redirectUri)
}
/// Cancels the authentication session.
public final func cancel() {
withIsolationSync {
await self.signInFlow.reset()
await self.signOutFlow?.reset()
}
provider?.cancel()
provider = nil
}
/// Initializes a web authentiation session using client credentials defined within the application's `Okta.plist` file.
public convenience init() throws {
try self.init(try OAuth2Client.PropertyListConfiguration())
}
/// Initializes a web authentication session using client credentials defined within the provided file URL.
/// - Parameter fileURL: File URL to a `plist` file containing client configuration.
public convenience init(plist fileURL: URL) throws {
try self.init(try OAuth2Client.PropertyListConfiguration(plist: fileURL))
}
/// Initializes a web authentication session using the supplied client credentials.
/// - Parameters:
/// - issuerURL: The URL for the OAuth2 issuer.
/// - clientId: The client's ID.
/// - scope: The scopes the client is requesting.
/// - redirectUri: The redirect URI for the configured client.
/// - logoutRedirectUri: The logout URI for the client, if applicable.
/// - additionalParameters: Optional parameters to add to the authorization query string.
public convenience init(issuerURL: URL,
clientId: String,
scope: ClaimCollection<[String]>,
redirectUri: URL,
logoutRedirectUri: URL? = nil,
additionalParameters: [String: any APIRequestArgument]? = nil) throws
{
let client = OAuth2Client(issuerURL: issuerURL,
clientId: clientId,
scope: scope,
redirectUri: redirectUri,
logoutRedirectUri: logoutRedirectUri)
try self.init(client: client, additionalParameters: additionalParameters)
}
@_documentation(visibility: private)
public convenience init(issuerURL: URL,
clientId: String,
scope: some WhitespaceSeparated,
redirectUri: URL,
logoutRedirectUri: URL? = nil,
additionalParameters: [String: any APIRequestArgument]? = nil) throws
{
let client = OAuth2Client(issuerURL: issuerURL,
clientId: clientId,
scope: scope,
redirectUri: redirectUri,
logoutRedirectUri: logoutRedirectUri)
try self.init(client: client, additionalParameters: additionalParameters)
}
convenience init(_ config: OAuth2Client.PropertyListConfiguration) throws {
try self.init(client: OAuth2Client(config),
additionalParameters: config.additionalParameters)
}
convenience init(client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]?) throws {
let loginFlow = try AuthorizationCodeFlow(client: client,
additionalParameters: additionalParameters)
let logoutFlow: SessionLogoutFlow?
if client.configuration.logoutRedirectUri != nil {
logoutFlow = SessionLogoutFlow(client: client,
additionalParameters: additionalParameters)
} else {
logoutFlow = nil
}
self.init(loginFlow: loginFlow, logoutFlow: logoutFlow)
}
/// Initializes a web authentication session using the supplied AuthorizationCodeFlow and optional context.
/// - Parameters:
/// - loginFlow: Authorization code flow instance for signing in to this client.
/// - logoutFlow: Session sign out flow to use when signing out from this client.
public init(loginFlow: AuthorizationCodeFlow, logoutFlow: SessionLogoutFlow?) {
assert(SDKVersion.webAuthenticationUI != nil)
self.signInFlow = loginFlow
self.signOutFlow = logoutFlow
WebAuthentication.shared = self
}
// MARK: Internal members
private static var _shared: WebAuthentication?
static var providerFactory: any WebAuthenticationProviderFactory.Type = WebAuthentication.self
// Used for testing only
static func resetToDefault() {
providerFactory = WebAuthentication.self
}
var provider: (any WebAuthenticationProvider)?
}
extension WebAuthentication: WebAuthenticationProviderFactory {
nonisolated static func createWebAuthenticationProvider(
for webAuth: WebAuthentication,
from window: WebAuthentication.WindowAnchor?,
usesEphemeralSession: Bool = false) throws -> (any WebAuthenticationProvider)?
{
try AuthenticationServicesProvider(from: window, usesEphemeralSession: usesEphemeralSession)
}
}
extension WebAuthentication {
/// Asynchronously initiates authentication from the given window.
/// - Parameters:
/// - window: The window from which the authentication browser should be shown.
/// - context: Context options used when composing the authorization URL.
/// - completion: Completion block that will be invoked when authentication finishes.
public final func signIn(from window: WindowAnchor?,
context: AuthorizationCodeFlow.Context = .init(),
completion: @escaping (Result<Token, WebAuthenticationError>) -> Void)
{
Task { @MainActor in
do {
completion(.success(try await signIn(from: window, context: context)))
} catch {
completion(.failure(WebAuthenticationError(error)))
}
}
}
/// Asynchronous convenience method that initiates log-out using the credential object, throwing the error if fails.
/// - Parameters:
/// - window: Window from which the sign in process will be started.
/// - credential: Stored credentials that will retrieve the ID token.
/// - context: Context options used when composing the signout URL.
/// - completion: Completion block that will be invoked when log-out finishes.
public final func signOut(from window: WindowAnchor?,
credential: Credential? = .default,
context: SessionLogoutFlow.Context = .init(),
completion: @escaping (Result<URL, WebAuthenticationError>) -> Void)
{
Task { @MainActor in
do {
completion(.success(try await signOut(from: window, credential: credential, context: context)))
} catch {
completion(.failure(WebAuthenticationError(error)))
}
}
}
/// Asynchronous convenience method that initiates log-out using the `Token` object, throwing the error if fails.
/// - Parameters:
/// - window: Window from which the sign in process will be started.
/// - token: Token object that will retrieve the ID token.
/// - context: Context options used when composing the signout URL.
/// - completion: Completion block that will be invoked when sign-out finishes.
public final func signOut(from window: WindowAnchor?,
token: Token,
context: SessionLogoutFlow.Context = .init(),
completion: @escaping (Result<URL, WebAuthenticationError>) -> Void)
{
Task { @MainActor in
do {
completion(.success(try await signOut(from: window, token: token, context: context)))
} catch {
completion(.failure(WebAuthenticationError(error)))
}
}
}
/// Asynchronous convenience method that initiates log-out using the ID Token, throwing the error if fails.
/// - Parameters:
/// - window: Window from which the sign in process will be started.
/// - context: Context options used when composing the signout URL.
public final func signOut(from window: WindowAnchor?,
context: SessionLogoutFlow.Context = .init(),
completion: @escaping (Result<URL, WebAuthenticationError>) -> Void)
{
Task { @MainActor in
do {
completion(.success(try await signOut(from: window, context: context)))
} catch {
completion(.failure(WebAuthenticationError(error)))
}
}
}
}