Skip to content

Commit 716acf5

Browse files
committed
Allow passing server metadata during OAuth2 initialization
1 parent 5fc1fe4 commit 716acf5

10 files changed

+72
-114
lines changed

Sources/Base/OAuth2Base.swift

Lines changed: 46 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -181,17 +181,21 @@ open class OAuth2Base: OAuth2Securable {
181181

182182
- verbose (Bool, false by default, applies to client logging)
183183
*/
184-
override public init(settings: OAuth2JSON) {
185-
clientConfig = OAuth2ClientConfig(settings: settings)
184+
override public init(settings: OAuth2JSON, serverMetadata: OAuth2ServerMetadata?) {
185+
clientConfig = OAuth2ClientConfig(settings: settings, serverMetadata: serverMetadata)
186186

187187
// auth configuration options
188188
if let ttl = settings["title"] as? String {
189189
authConfig.ui.title = ttl
190190
}
191-
super.init(settings: settings)
191+
192+
super.init(settings: settings, serverMetadata: serverMetadata)
193+
194+
if let serverMetadata {
195+
self.validateConfiguration(against: serverMetadata)
196+
}
192197
}
193198

194-
195199
// MARK: - Keychain Integration
196200

197201
/** Overrides base implementation to return the authorize URL. */
@@ -290,28 +294,39 @@ open class OAuth2Base: OAuth2Securable {
290294
/**
291295
Internally used on error, calls the callbacks on the main thread with the appropriate error message.
292296

293-
This method is only made public in case you want to create a subclass and need to call `didFail(error:)` at an override point. If you
294-
call this method yourself on your OAuth2 instance you might screw things up royally.
297+
This method handles authorization failures by cleaning up state, logging the error,
298+
and notifying all registered callbacks. It ensures proper cleanup of async continuations
299+
and maintains thread safety by dispatching callbacks to the main thread.
300+
301+
This method is only made public in case you want to create a subclass and need to call
302+
`didFail(with:)` at an override point. If you call this method yourself on your OAuth2
303+
instance you might cause unexpected behavior.
295304

296-
- parameter error: The error that led to authorization failure; will use `.requestCancelled` on the callbacks if nil is passed
305+
- parameter error: The error that led to authorization failure; will use `.requestCancelled` if nil is passed
297306
*/
298307
public final func didFail(with error: OAuth2Error?) {
299-
var finalError = error
300-
if let error = finalError {
301-
logger?.debug("OAuth2", msg: "\(error)")
302-
}
303-
else {
304-
finalError = OAuth2Error.requestCancelled
308+
let finalError = error ?? OAuth2Error.requestCancelled
309+
310+
// Log the error with appropriate level
311+
if let error = error {
312+
logger?.warn("OAuth2", msg: "Authorization failed with error: \(error)")
313+
} else {
314+
logger?.debug("OAuth2", msg: "Authorization was cancelled or aborted")
305315
}
316+
317+
// Dispatch callbacks to main thread with proper error handling
306318
callOnMainThread() {
307319
self.isAuthorizing = false
308320
self.internalAfterAuthorizeOrFail?(true, finalError)
309321
self.afterAuthorizeOrFail?(nil, finalError)
310322
}
311323

312-
// Finish `doAuthorize` call
313-
self.doAuthorizeContinuation?.resume(throwing: error ?? OAuth2Error.requestCancelled)
314-
self.doAuthorizeContinuation = nil
324+
// Complete async continuation if present
325+
if let continuation = doAuthorizeContinuation {
326+
continuation.resume(throwing: finalError)
327+
doAuthorizeContinuation = nil
328+
logger?.trace("OAuth2", msg: "Completed async authorization continuation with error")
329+
}
315330
}
316331

317332
/**
@@ -504,87 +519,27 @@ open class OAuth2Base: OAuth2Securable {
504519
open func assureRefreshTokenParamsAreValid(_ params: OAuth2JSON) throws {
505520
}
506521

507-
}
508-
509-
510-
/**
511-
Class, internally used, to store current authorization context, such as state and redirect-url.
512-
*/
513-
open class OAuth2ContextStore {
514-
515-
/// Currently used redirect_url.
516-
open var redirectURL: String?
517-
518-
/// Current code verifier used for PKCE
519-
public internal(set) var codeVerifier: String?
520-
public let codeChallengeMethod = "S256"
521-
522-
/// The current state.
523-
internal var _state = ""
524-
525522
/**
526-
The state sent to the server when requesting a token.
523+
Validates the current OAuth2 configuration against server metadata.
527524

528-
We internally generate a UUID and use the first 8 chars if `_state` is empty.
529-
*/
530-
open var state: String {
531-
if _state.isEmpty {
532-
_state = UUID().uuidString
533-
_state = String(_state[_state.startIndex..<_state.index(_state.startIndex, offsetBy: 8)]) // only use the first 8 chars, should be enough
534-
}
535-
return _state
536-
}
525+
This method checks if the configured grant type and response type are supported
526+
by the authorization server according to the provided metadata.
537527

538-
/**
539-
Checks that given state matches the internal state.
540-
541-
- parameter state: The state to check (may be nil)
542-
- returns: true if state matches, false otherwise or if given state is nil.
528+
- parameter serverMetadata: The server metadata to validate against
543529
*/
544-
func matchesState(_ state: String?) -> Bool {
545-
if let st = state {
546-
return st == _state
530+
open func validateConfiguration(against serverMetadata: OAuth2ServerMetadata) {
531+
// validate the grant type
532+
let grantType = type(of: self).grantType
533+
let grantTypesSupported = serverMetadata.grantTypesSupported ?? [OAuth2GrantTypes.authorizationCode, OAuth2GrantTypes.implicit]
534+
if !grantTypesSupported.contains(grantType) {
535+
logger?.warn("OAuth2", msg: "The authorization server doesn't support the \"\(grantType)\" grant type.")
547536
}
548-
return false
549-
}
550-
551-
/**
552-
Resets current state so it gets regenerated next time it's needed.
553-
*/
554-
func resetState() {
555-
_state = ""
556-
}
557-
558-
// MARK: - PKCE
559-
560-
/**
561-
Generates a new code verifier string
562-
*/
563-
open func generateCodeVerifier() {
564-
var buffer = [UInt8](repeating: 0, count: 32)
565-
_ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
566-
codeVerifier = Data(buffer).base64EncodedString()
567-
.replacingOccurrences(of: "+", with: "-")
568-
.replacingOccurrences(of: "/", with: "_")
569-
.replacingOccurrences(of: "=", with: "")
570-
.trimmingCharacters(in: .whitespaces)
571-
}
572-
573-
574-
open func codeChallenge() -> String? {
575-
guard let verifier = codeVerifier, let data = verifier.data(using: .utf8) else { return nil }
576-
var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
577-
data.withUnsafeBytes {
578-
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &buffer)
537+
538+
// validate the response type
539+
if let responseType = type(of: self).responseType,
540+
!serverMetadata.responseTypesSupported.contains(responseType) {
541+
logger?.warn("OAuth2", msg: "The authorization server doesn't support the \"\(responseType)\" response type.")
579542
}
580-
let hash = Data(buffer)
581-
let challenge = hash.base64EncodedString()
582-
.replacingOccurrences(of: "+", with: "-")
583-
.replacingOccurrences(of: "/", with: "_")
584-
.replacingOccurrences(of: "=", with: "")
585-
.trimmingCharacters(in: .whitespaces)
586-
return challenge
587543
}
588544

589545
}
590-

Sources/Base/OAuth2ClientConfig.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,22 +120,25 @@ open class OAuth2ClientConfig {
120120
/**
121121
Initializer to initialize properties from a settings dictionary.
122122
*/
123-
public init(settings: OAuth2JSON) {
123+
public init(settings: OAuth2JSON, serverMetadata: OAuth2ServerMetadata?) {
124124
clientId = settings["client_id"] as? String
125125
clientSecret = settings["client_secret"] as? String
126126
clientName = settings["client_name"] as? String
127-
128-
// authorize URL
127+
129128
var aURL: URL?
130-
if let auth = settings["authorize_uri"] as? String {
131-
aURL = URL(string: auth)
129+
if let authorizeUri = settings["authorize_uri"] as? String {
130+
aURL = URL(string: authorizeUri)
131+
} else if let authorizationEndpoint = serverMetadata?.authorizationEndpoint {
132+
aURL = URL(string: authorizationEndpoint)
132133
}
133134
authorizeURL = aURL ?? URL(string: "https://localhost/p2.OAuth2.defaultAuthorizeURI")!
134135

135-
// token, device code, registration and logo URLs
136-
if let token = settings["token_uri"] as? String {
137-
tokenURL = URL(string: token)
136+
if let tokenUri = settings["token_uri"] as? String {
137+
tokenURL = URL(string: tokenUri)
138+
} else if let tokenEndpoint = serverMetadata?.tokenEndpoint {
139+
tokenURL = URL(string: tokenEndpoint)
138140
}
141+
139142
if let deviceAuthorize = settings["device_authorize_uri"] as? String {
140143
deviceAuthorizeURL = URL(string: deviceAuthorize)
141144
}

Sources/Base/OAuth2Securable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ open class OAuth2Securable: OAuth2Requestable {
6565

6666
Looks at the `verbose`, `keychain`, `keychain_access_mode`, `keychain_access_group` `keychain_account_for_client_credentials` and `keychain_account_for_tokens`. Everything else is handled by subclasses.
6767
*/
68-
public init(settings: OAuth2JSON) {
68+
public init(settings: OAuth2JSON, serverMetadata: OAuth2ServerMetadata?) {
6969
self.settings = settings
7070

7171
// keychain settings

Sources/Flows/OAuth2.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ open class OAuth2: OAuth2Base {
8383

8484
- verbose (bool, false by default, applies to client logging)
8585
*/
86-
override public init(settings: OAuth2JSON) {
87-
super.init(settings: settings)
88-
self.authorizer = OAuth2Authorizer(oauth2: self)
89-
86+
override public init(settings: OAuth2JSON, serverMetadata: OAuth2ServerMetadata? = nil) {
87+
super.init(settings: settings, serverMetadata: serverMetadata)
88+
authorizer = OAuth2Authorizer(oauth2: self)
89+
9090
if (self.clientConfig.refreshTokenRotationIsEnabled) {
9191
self.refreshTokenRotationSemaphore = AsyncSemaphore(value: 1)
9292
}

Sources/Flows/OAuth2ClientCredentialsReddit.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ public class OAuth2ClientCredentialsReddit: OAuth2ClientCredentials {
4747

4848
- parameter settings: The authorization settings
4949
*/
50-
override public init(settings: OAuth2JSON) {
50+
override public init(settings: OAuth2JSON, serverMetadata: OAuth2ServerMetadata?) {
5151
deviceId = settings["device_id"] as? String
52-
super.init(settings: settings)
52+
super.init(settings: settings, serverMetadata: serverMetadata)
5353
clientConfig.clientSecret = ""
5454
}
5555

Sources/Flows/OAuth2CodeGrantAzure.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ public class OAuth2CodeGrantAzure: OAuth2CodeGrant {
3636
- parameter settings: The settings for this client
3737
- parameter resource: The resource we want to use
3838
*/
39-
public init(settings: OAuth2JSON, resource: String) {
40-
super.init(settings: settings)
39+
public init(settings: OAuth2JSON, serverMetadata: OAuth2ServerMetadata?, resource: String) {
40+
super.init(settings: settings, serverMetadata: serverMetadata)
4141
clientConfig.secretInBody = true
4242
clientConfig.customParameters = [
4343
"resource": resource

Sources/Flows/OAuth2CodeGrantBasicAuth.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ open class OAuth2CodeGrantBasicAuth: OAuth2CodeGrant {
4242

4343
- basic: takes precedence over client_id and client_secret for the token request Authorization header
4444
*/
45-
override public init(settings: OAuth2JSON) {
45+
override public init(settings: OAuth2JSON, serverMetadata: OAuth2ServerMetadata?) {
4646
if let basic = settings["basic"] as? String {
4747
basicToken = basic
4848
}
49-
super.init(settings: settings)
49+
super.init(settings: settings, serverMetadata: serverMetadata)
5050
}
5151

5252
/**

Sources/Flows/OAuth2DeviceGrant.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ open class OAuth2DeviceGrant: OAuth2 {
3232
}
3333

3434
override open class var responseType: String? {
35-
return ""
35+
return nil
3636
}
3737

3838
open func deviceAccessTokenRequest(with deviceCode: String) throws -> OAuth2AuthRequest {

Sources/Flows/OAuth2ImplicitGrant.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Constants
2727

2828

2929
/**
30-
Class to handle OAuth2 requests for public clients, such as distributed Mac/iOS Apps.
30+
Class to handle OAuth2 requests for public clients, such as distributed macOS/iOS Apps.
3131
*/
3232
open class OAuth2ImplicitGrant: OAuth2 {
3333

Sources/Flows/OAuth2PasswordGrant.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ open class OAuth2PasswordGrant: OAuth2 {
8686
/**
8787
Adds support for the "password" & "username" setting.
8888
*/
89-
override public init(settings: OAuth2JSON) {
89+
override public init(settings: OAuth2JSON, serverMetadata: OAuth2ServerMetadata? = nil) {
9090
username = settings["username"] as? String
9191
password = settings["password"] as? String
92-
super.init(settings: settings)
92+
super.init(settings: settings, serverMetadata: serverMetadata)
9393
}
9494

9595

0 commit comments

Comments
 (0)