OAuth2 framework for macOS and iOS, written in Swift.

Overview

OAuth2

Build Status License

OAuth2 frameworks for macOS, iOS and tvOS written in Swift 5.0.

OAuth2 requires Xcode 10.2, the built framework can be used on OS X 10.11 or iOS 8 and later. Happy to accept pull requests, please see CONTRIBUTING.md

Swift Version

Since the Swift language is constantly evolving I have adopted a versioning scheme mirroring Swift versions: the framework version's first two digits are always the Swift version the library is compatible with, see releases. Code compatible with brand new Swift versions are to be found on a separate feature branch named appropriately.

Usage

To use OAuth2 in your own code, start with import OAuth2 in your source files.

In OAuth2 there are different kinds of flows. This library supports all of them, make sure you're using the correct one for your use-case and authorization server. A typical code grant flow is used for demo purposes below. The steps for other flows are mostly the same short of instantiating a different subclass and using different client settings.

Still not working? See site-specific peculiarities.

1. Instantiate OAuth2 with a Settings Dictionary

In this example you'll be building an iOS client to Github, so the code below will be somewhere in a view controller of yours, maybe the app delegate.

let oauth2 = OAuth2CodeGrant(settings: [
    "client_id": "my_swift_app",
    "client_secret": "C7447242",
    "authorize_uri": "https://github.com/login/oauth/authorize",
    "token_uri": "https://github.com/login/oauth/access_token",   // code grant only
    "redirect_uris": ["myapp://oauth/callback"],   // register your own "myapp" scheme in Info.plist
    "scope": "user repo:status",
    "secret_in_body": true,    // Github needs this
    "keychain": false,         // if you DON'T want keychain integration
] as OAuth2JSON)

See those redirect_uris? You can use the scheme you want, but you must a) declare the scheme you use in your Info.plist and b) register the very same URI on the authorization server you connect to.

Note that as of iOS 9, you should use Universal Links as your redirect URL, rather than a custom app scheme. This prevents others from re-using your URI scheme and intercept the authorization flow.
If you target iOS 12 and newer you should be using ASWebAuthenticationSession, which makes using your own local redirect scheme secure.

Want to avoid switching to Safari and pop up a SafariViewController or NSPanel? Set this:

oauth2.authConfig.authorizeEmbedded = true
oauth2.authConfig.authorizeContext = <# your UIViewController / NSWindow #>

Need to specify a separate refresh token URI? You can set the refresh_uri in the Settings Dictionary. If specified the library will refresh access tokens using the refresh_uri you specified, otherwise it will use the token_uri.

Need to debug? Use a .debug or even a .trace logger:

oauth2.logger = OAuth2DebugLogger(.trace)

For more see advanced settings below.

2. Let the Data Loader or Alamofire Take Over

Starting with version 3.0, there is an OAuth2DataLoader class that you can use to retrieve data from an API. It will automatically start authorization if needed and will ensure that this works even if you have multiple calls going on. For details on how to configure authorization see step 4 below, in this example we'll use "embedded" authorization, meaning we'll show a SFSafariViewController on iOS if the user needs to log in.

This wiki page has all you need to easily use OAuth2 with Alamofire instead.

let base = URL(string: "https://api.github.com")!
let url = base.appendingPathComponent("user")

var req = oauth2.request(forURL: url)
req.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")

self.loader = OAuth2DataLoader(oauth2: oauth2)
loader.perform(request: req) { response in
    do {
        let dict = try response.responseJSON()
        DispatchQueue.main.async {
            // you have received `dict` JSON data!
        }
    }
    catch let error {
        DispatchQueue.main.async {
            // an error occurred
        }
    }
}

3. Make Sure You Intercept the Callback

When using the OS browser or the iOS 9+ Safari view controller, you will need to intercept the callback in your app delegate and let the OAuth2 instance handle the full URL:

func application(_ app: UIApplication,
              open url: URL,
               options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
    // you should probably first check if this is the callback being opened
    if <# check #> {
        // if your oauth2 instance lives somewhere else, adapt accordingly
        oauth2.handleRedirectURL(url)
    }
}

For iOS 13 make the callback in SceneDelegate.swift

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
	if let url = URLContexts.first?.url {
		AppDelegate.shared.oauth2?.handleRedirectURL(url)
	}
}

You’re all set!


If you want to dig deeper or do authorization yourself, here it goes:

4. Manually Authorize the User

By default the OS browser will be used for authorization if there is no access token present or in the keychain. Starting with iOS 12, ASWebAuthenticationSession will be used when enabling embedded authorization on iOS (previously, starting with iOS 9, SFSafariViewController was used instead).

To start authorization call authorize(params:callback:) or, to use embedded authorization, the convenience method authorizeEmbedded(from:callback:).

The login screen will only be presented if needed (see _Manually Performing Authorization below for details) and will automatically dismiss the login screen on success. See Advanced Settings for other options.

oauth2.authorize() { authParameters, error in
    if let params = authParameters {
        print("Authorized! Access token is in `oauth2.accessToken`")
        print("Authorized! Additional parameters: \(params)")
    }
    else {
        print("Authorization was canceled or went wrong: \(error)")   // error will not be nil
    }
}

// for embedded authorization you can simply use:
oauth2.authorizeEmbedded(from: <# presenting view controller / window #>) { ... }

// which is equivalent to:
oauth2.authConfig.authorizeEmbedded = true
oauth2.authConfig.authorizeContext = <# presenting view controller / window #>
oauth2.authorize() { ... }

Don't forget, when using the OS browser or the iOS 9+ Safari view controller, you will need to intercept the callback in your app delegate. This is shown under step 2 above.

See Manually Performing Authorization below for details on how to do this on the Mac.

5. Receive Callback

After everything completes the callback will be called, either with a non-nil authParameters dictionary (which may be empty!), or an error. The access and refresh tokens and its expiration dates will already have been extracted and are available as oauth2.accessToken and oauth2.refreshToken parameters. You only need to inspect the authParameters dictionary if you wish to extract additional information.

For advanced use outlined below, there is the afterAuthorizeOrFail block that you can use on your OAuth2 instance. The internalAfterAuthorizeOrFail closure is, as its name suggests, provided for internal purposes – it is exposed for subclassing and compilation reasons and you should not mess with it. As of version 3.0.2, you can no longer use the onAuthorize and onFailure callback properties, they have been removed entirely.

6. Make Requests

You can now obtain an OAuth2Request, which is an already signed MutableURLRequest, to retrieve data from your server. This request sets the Authorization header using the access token like so: Authorization: Bearer {your access token}.

let req = oauth2.request(forURL: <# resource URL #>)
// set up your request, e.g. `req.HTTPMethod = "POST"`
let task = oauth2.session.dataTaskWithRequest(req) { data, response, error in
    if let error = error {
        // something went wrong, check the error
    }
    else {
        // check the response and the data
        // you have just received data with an OAuth2-signed request!
    }
}
task.resume()

Of course you can use your own URLSession with these requests, you don't have to use oauth2.session; use OAuth2DataLoader, as shown in step 2, or hand it over to Alamofire. Here's all you need to easily use OAuth2 with Alamofire.

7. Cancel Authorization

You can cancel an ongoing authorization any time by calling oauth2.abortAuthorization(). This will cancel ongoing requests (like a code exchange request) or call the callback while you're waiting for a user to login on a webpage. The latter will dismiss embedded login screens or redirect the user back to the app.

8. Re-Authorize

It is safe to always call oauth2.authorize() before performing a request. You can also perform the authorization before the first request after your app became active again. Or you can always intercept 401s in your requests and call authorize again before re-attempting the request.

9. Logout

If you're storing tokens to the keychain, you can call forgetTokens() to throw them away.

However your user is likely still logged in to the website, so on the next authorize() call, the web view may appear and immediately disappear. When using the built-in web view on iOS 8, one can use the following snippet to throw away any cookies the app created. With the newer SFSafariViewController, or logins performed in the browser, it's probably best to directly open the logout page so the user sees the logout happen.

let storage = HTTPCookieStorage.shared
storage.cookies?.forEach() { storage.deleteCookie($0) }

Manually Performing Authorization

The authorize(params:callback:) method will:

  1. Check if an authorize call is already running, if yes it will abort with an OAuth2Error.alreadyAuthorizing error
  2. Check if an access token that has not yet expired is already present (or in the keychain), if not
  3. Check if a refresh token is available, if found
  4. Try to use the refresh token to get a new access token, if it fails
  5. Start the OAuth2 dance by using the authConfig settings to determine how to display an authorize screen to the user

Your oauth2 instance will use an automatically created URLSession using an ephemeralSessionConfiguration() configuration for its requests, exposed on oauth2.session. You can set oauth2.sessionConfiguration to your own configuration, for example if you'd like to change timeout values. You can also set oauth2.sessionDelegate to your own session delegate if you like.

The wiki has the complete call graph of the authorize() method. If you do not wish this kind of automation, the manual steps to show and hide the authorize screens are:

Embedded iOS:

let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
oauth2.authConfig.authorizeEmbeddedAutoDismiss = false
let web = try oauth2.authorizer.authorizeSafariEmbedded(from: <# view controller #>, at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
    // inspect error or oauth2.accessToken / authParameters or do something else
    web.dismissViewControllerAnimated(true, completion: nil)
}

Modal Sheet on macOS:

let window = <# window to present from #>
let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
let sheet = try oauth2.authorizer.authorizeEmbedded(from: window, at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
    // inspect error or oauth2.accessToken / authParameters or do something else
    window.endSheet(sheet)
}

New window on macOS:

let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
let windowController = try oauth2.authorizer.authorizeInNewWindow(at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
    // inspect error or oauth2.accessToken / authParameters or do something else
    windowController.window?.close()
}

iOS/macOS browser:

let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
try oauth2.authorizer.openAuthorizeURLInBrowser(url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
    // inspect error or oauth2.accessToken / authParameters or do something else
}

macOS

See the OAuth2 Sample App's AppDelegate class on how to receive the callback URL in your Mac app. If the authorization displays the code to the user, e.g. with Google's urn:ietf:wg:oauth:2.0:oob callback URL, you can retrieve the code from the user's pasteboard and continue authorization with:

let pboard = NSPasteboard.general()
if let pasted = pboard.string(forType: NSPasteboardTypeString) {
    oauth2.exchangeCodeForToken(pasted)
}

Flows

Based on which OAuth2 flow that you need you will want to use the correct subclass. For a very nice explanation of OAuth's basics: The OAuth Bible.

Code Grant

For a full OAuth 2 code grant flow (response_type=code) you want to use the OAuth2CodeGrant class. This flow is typically used by applications that can guard their secrets, like server-side apps, and not in distributed binaries. In case an application cannot guard its secret, such as a distributed iOS app, you would use the implicit grant or, in some cases, still a code grant but omitting the client secret. It has however become common practice to still use code grants from mobile devices, including a client secret.

This class fully supports those flows, it automatically creates a “Basic” Authorization header if the client has a non-nil client secret. This means that you likely must specify client_secret in your settings; if there is none (like for Reddit) specify the empty string. If the site requires client credentials in the request body, set clientConfig.secretInBody to true, as explained below.

Implicit Grant

An implicit grant (response_type=token) is suitable for apps that are not capable of guarding their secret, such as distributed binaries or client-side web apps. Use the OAuth2ImplicitGrant class to receive a token and perform requests.

Would be nice to add another code example here, but it's pretty much the same as for the code grant.

Client Credentials

A 2-legged flow that lets an app authorize itself via its client id and secret. Instantiate OAuth2ClientCredentials, as usual supplying client_id but also a client_secret – plus your other configurations – in the settings dict, and you should be good to go.

Username and Password

The Resource Owner Password Credentials Grant is supported with the OAuth2PasswordGrant subclass. Create an instance as shown above, set its username and password properties, then call authorize().

Site-Specific Peculiarities

Some sites might not strictly adhere to the OAuth2 flow, from returning data differently like Facebook to omitting mandatory return parameters like Instagram & co. The framework deals with those deviations by creating site-specific subclasses and/or configuration details. If you need to pass additional headers or parameters, you can supply these in the settings dict like so:

let oauth2 = OAuth2CodeGrant(settings: [
    "client_id": "...",
    ...
    "headers": ["Accept": "application/vnd.github.v3+json"],
    "parameters": ["duration": "permanent"],
] as OAuth2JSON)

Advanced Settings

The main configuration you'll use with oauth2.authConfig is whether or not to use an embedded login:

oauth2.authConfig.authorizeEmbedded = true

Similarly, if you want to take care of dismissing the login screen yourself (not possible with the newer authorization sessions mentioned below):

oauth2.authConfig.authorizeEmbeddedAutoDismiss = false

Some sites also want the client-id/secret combination in the request body, not in the Authorization header:

oauth2.clientConfig.secretInBody = true
// or in your settings:
"secret_in_body": true

Sometimes you also need to provide additional authorization parameters. This can be done in 3 ways:

oauth2.authParameters = ["duration": "permanent"]
// or in your settings:
"parameters": ["duration": "permanent"]
// or when you authorize manually:
oauth2.authorize(params: ["duration": "permanent"]) { ... }

Similar is how you specify custom HTTP headers:

oauth2.clientConfig.authHeaders = ["Accept": "application/json, text/plain"]
// or in your settings:
"headers": ["Accept": "application/json, text/plain"]

Starting with version 2.0.1 on iOS 9, SFSafariViewController will be used for embedded authorization. Starting after version 4.2, on iOS 11 (SFAuthenticationSession) and iOS 12 (ASWebAuthenticationSession), you can opt-in to these newer authorization session view controllers:

oauth2.authConfig.ui.useAuthenticationSession = true

To revert to the old custom OAuth2WebViewController, which you should not do because ASWebAuthenticationSession is way more secure:

oauth2.authConfig.ui.useSafariView = false

To customize the go back button when using OAuth2WebViewController on iOS 8 and older:

oauth2.authConfig.ui.backButton = <# UIBarButtonItem(...) #>

See below for settings about the keychain and PKCE.

Usage with Alamofire

You'll get the best experience when using Alamofire v4 or newer and OAuth2 v3 and newer:

Dynamic Client Registration

There is support for dynamic client registration. If during setup registration_url is set but client_id is not, the authorize() call automatically attempts to register the client before continuing to the actual authorization. Client credentials returned from registration are stored to the keychain.

The OAuth2DynReg class is responsible for handling client registration. You can use its register(client:callback:) method manually if you need to. Registration parameters are taken from the client's configuration.

let oauth2 = OAuth2...()
oauth2.registerClientIfNeeded() { error in
    if let error = error {
        // registration failed
    }
    else {
        // client was registered
    }
}
let oauth2 = OAuth2...()
let dynreg = OAuth2DynReg()
dynreg.register(client: oauth2) { params, error in
    if let error = error {
        // registration failed
    }
    else {
        // client was registered with `params`
    }
}

PKCE

PKCE support is controlled by the useProofKeyForCodeExchange property, and the use_pkce key in the settings dictionary. It is disabled by default. When enabled, a new code verifier string is generated for every authorization request.

Keychain

This framework can transparently use the iOS and macOS keychain. It is controlled by the useKeychain property, which can be disabled during initialization with the keychain settings dictionary key. Since this is enabled by default, if you do not turn it off during initialization, the keychain will be queried for tokens and client credentials related to the authorization URL. If you turn it off after initialization, the keychain will be queried for existing tokens, but new tokens will not be written to the keychain.

If you want to delete the tokens from keychain, i.e. log the user out completely, call forgetTokens(). If you have dynamically registered your client and want to start anew, you can call forgetClient().

Ideally, access tokens get delivered with an "expires_in" parameter that tells you how long the token is valid. If it is missing the framework will still use those tokens if one is found in the keychain and not re-perform the OAuth dance. You will need to intercept 401s and re-authorize if an access token has expired but the framework has still pulled it from the keychain. This behavior can be turned off by supplying token_assume_unexpired: false in settings or setting clientConfig.accessTokenAssumeUnexpired to false.

These are the settings dictionary keys you can use for more control:

  • keychain: a bool on whether to use keychain or not, true by default
  • keychain_access_mode: a string value for keychain kSecAttrAccessible attribute, "kSecAttrAccessibleWhenUnlocked" by default, you can change this to e.g. "kSecAttrAccessibleAfterFirstUnlock" if you need the tokens to be available when the phone is locked.
  • keychain_access_group: a string value for keychain kSecAttrAccessGroup attribute, nil by default
  • keychain_account_for_client_credentials: the name to use to identify client credentials in the keychain, "clientCredentials" by default
  • keychain_account_for_tokens: the name to use to identify the tokens in the keychain, "currentTokens" by default

Installation

You can use the Swift Package Manager, git or Carthage. The preferred way is to use the Swift Package Manager.

Swift Package Manager

In Xcode 11 and newer, choose "File" from the Xcode Menu, then "Swift Packages" » "Add Package Dependency..." and paste the URL of this repo: https://github.com/p2/OAuth2.git. Pick a version and Xcode should do the rest.

Carthage

Installation via Carthage is easy enough:

github "p2/OAuth2" ~> 4.2

git

Using Terminal.app, clone the OAuth2 repository, best into a subdirectory of your app project:

$ cd path/to/your/app
$ git clone --recursive https://github.com/p2/OAuth2.git

If you're using git you'll want to add it as a submodule. Once cloning completes, open your app project in Xcode and add OAuth2.xcodeproj to your app:

Adding to Xcode

Now link the framework to your app:

Linking

These three steps are needed to:

  1. Make your App also build the framework
  2. Link the framework into your app
  3. Embed the framework in your app when distributing

License

This code is released under the Apache 2.0 license, which means that you can use it in open as well as closed source projects. Since there is no NOTICE file there is nothing that you have to include in your product.

Comments
  • OAuth2PasswordGrant should parse refresh token

    OAuth2PasswordGrant should parse refresh token

    Hi,

    I'm using OAuth2PasswordGrant in my project and I don't think the "password" flow is fully implemented.

    First, OAuth2PasswordGrant will try to use the authorization_uri instead of the token_uri to request and access_token using the username and password. (This can be hacked by using the a token uri as the authorization_uri value).

    Second, I believe all grant types allow to refresh access_token using the refresh_token. I just see this implemented in OAuth2CodeGrant. I think all the refresh logic (refreshToken, doRefreshToken(), ...) should be shared with OAuth2PasswordGrant.

    Cheers.

    enhancement 
    opened by damienrambout 37
  • Swift 3.0 - No such module

    Swift 3.0 - No such module

    Hello,

    I'm trying to build my project with 3.0, but seems to be not possible at this time ?

    I tried with : pod 'p2.OAuth2', :git => 'https://github.com/p2/OAuth2', :branch => 'develop'

    But got this : capture d ecran 2016-09-14 a 15 01 02

    Have you any solution ? :-)

    opened by Goule 32
  • Use of undeclared type 'HTTPURLResponse'

    Use of undeclared type 'HTTPURLResponse'

    Hi,

    I have just installed the Pod using

    pod 'p2.OAuth2', :git => 'https://github.com/p2/OAuth2', :submodules => true'

    And i simply run the app it showed me 445 errors, I have attached screenshot herewith.

    screen shot 2016-10-13 at 10 17 47 am

    screen shot 2016-10-13 at 10 16 00 am

    opened by aadilimperoit 28
  • Can't get past Authorize

    Can't get past Authorize

    I have been stuck for days now. This is a site-specific issue. I am trying to obtain an access token using MacOS and embedded view. I can log in to the site and it asks me properly to authorize access. However, clicking authorize does nothing. I don't even see a response (i have used .trace to get the initial "no access token" response from the server. Any ideas on how to find where the hang up is?

    I have modified the BitBucket data loader to accept embedding, my client id, secret, etc.

    Any ideas?

    Is this redirect_uri issue?

    Thanks

    screen shot 2017-01-26 at 3 07 36 pm
    opened by jcedman 26
  • String is not identifcal to NSObject

    String is not identifcal to NSObject

    Following the Usage steps and I get:

    eh

    Also, minor, but there's no note about the need for import Oauth2

    I'm new, perhaps I missed a step and am causing my own problems? Note: xCode 6.2 (6C131e)

    opened by danshev 25
  • Strange keychain error

    Strange keychain error

    [Warn!] OAuth2: Failed to load client credentials from keychain: Error Domain=swift.keychain.error Code=-50 "(null)"
    [Warn!] OAuth2: Failed to load tokens from keychain: Error Domain=swift.keychain.error Code=-50 "(null)"
    

    I'm able to authorize, but I'm getting this cryptic error. Any ideas? I'm targeting iOS 9 and using your dev branch.

    bug 
    opened by dylan 20
  • ClientConfig: Warn about invalid settings

    ClientConfig: Warn about invalid settings

    Given my embarrassing mistake in #264 I thought it might be useful to add a warning about invalid settings.

    Not sure if this can be merged as is. I had to create a new logger instance in OAuth2ClientConfig.init because I didn't know how else to get access to a logger.

    opened by FSMaxB-divae 19
  • No redirection after Google+ authentication

    No redirection after Google+ authentication

    Performed a OAuth2CodeGrant operation with Google+ settings:

    let settings = [
        "client_id": xxxx,
        "authorize_uri": "https://accounts.google.com/o/oauth2/auth",
        "token_uri": "https://accounts.google.com/o/oauth2/token",
        "scope": "https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email",
        "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob:auto"],
    ]
    

    The authentication succeeded by the OAuth2WebView was not dismissed automatically.

    I put a breakpoint in shouldStartLoadWithRequest method of the OAuth2WebViewController and noticed that request.URL.scheme does not equal to interceptComponents?.scheme and the same for request.URL.host and interceptComponents?.host. So onInterceptcallback is never called.

    site-specifics 
    opened by rjourde 19
  • Problem with parallel requests

    Problem with parallel requests

    When the access token expires and multiple requests happen, that all request a new access token via the refresh token, at the same time, some of them fail (because the access token from the first refresh token request does not work after the second refresh token request).

    Parallel requests can be triggered from different parts of the app. For example when the app is launched the current location is sent to the server and a list of events is downloaded.

    What would be the best way to make sure that no second refresh token request is made while the first is still running?

    opened by tompson 17
  • SFSafariViewController not implemented

    SFSafariViewController not implemented

    I'm not too sure where to ask this (GitHub repositories don't really have feature request pages), but is it possible to add SFSafariViewController to this framework? Now that we can submit iOS 9 apps, I'm sure that it would be a much better user experience.

    enhancement 
    opened by kabiroberai 17
  • Added native support for Password Grant flow

    Added native support for Password Grant flow

    As discussed in #178 , here is a draft for native support in password grant. I created a new OAuth2PasswordGrantCustom to separate it from the original webview flow. For now it just bypass the client registration step, as the RFC doesn't mention any requirement of a client_id being send as a parameter of the accessToken request. I copied the OAuth2AuthorizeUI and OAuth2Authorizer pattern with OAuth2LoginPresentable and OAuth2LoginPresenter (macOS is not supported yet though)

    Let me now if there is something I'm missing or wrong.

    opened by amaurydavid 16
  • Update OAuth2Authorizer+iOS.swift

    Update OAuth2Authorizer+iOS.swift

    Update OAuth2Authorizer+iOS.swift macCatalyst 13.1 need setting presentationContextProvider. If not will get an error:

    Printing description of error:
    ▿ Optional<Error>
      - some : Error Domain=com.apple.AuthenticationServices.WebAuthenticationSession Code=2 "Cannot start ASWebAuthenticationSession without providing presentation context. Set presentationContextProvider before calling -start." UserInfo={NSDebugDescription=Cannot start ASWebAuthenticationSession without providing presentation context. Set presentationContextProvider before calling -start.}
    
    opened by rushairer 0
  • mac catalyst can not open login window

    mac catalyst can not open login window

    OAuth 5.3.2/ SwiftUI/ using macOS 13 run catalyst mode, will get this error:

    2022-11-17 15:42:03.532115+0800 Crossword[1778:4730281] [TraitCollection] Class _UIFindNavigatorViewController overrides the -traitCollection getter, which is not supported. If you're trying to override traits, you must use the appropriate API.
    2022-11-17 15:42:20.836892+0800 Crossword[1778:4730281] [AXRuntimeCommon] Unknown client: Crossword
    [Debug] OAuth2: Starting authorization
    [Debug] OAuth2: No access token, checking if a refresh token is available
    [Debug] OAuth2: Error refreshing token: I don't have a refresh token, not trying to refresh
    [Debug] OAuth2: Opening authorize URL embedded:  *****************HIDDEN*****************
    [Debug] OAuth2: The operation couldn’t be completed. (com.apple.AuthenticationServices.WebAuthenticationSession error 2.)
    Authorization was canceled or went wrong: The operation couldn’t be completed. (com.apple.AuthenticationServices.WebAuthenticationSession error 2.).
    
    

    Both iOS/Mac are OK, only catalyst can not work :(

    
    import OAuth2
    #if os(iOS)
    import UIKit
    #elseif os(macOS)
    import AppKit
    #endif
    import Combine
    import WebKit
    
    public struct OAuth2Configuration {
        public init(clientId: String, authorizeUri: String, tokenUri: String, redirectUris: [String], scope: String, customParameters: OAuth2StringDict) {
            self.clientId = clientId
            self.authorizeUri = authorizeUri
            self.tokenUri = tokenUri
            self.redirectUris = redirectUris
            self.scope = scope
            self.customParameters = customParameters
        }
        
        let clientId: String
        let authorizeUri: String
        let tokenUri: String
        let redirectUris: [String]
        let scope: String
        let customParameters: OAuth2StringDict
        let logLevel: OAuth2LogLevel = .debug
    }
    
    public class OAuth2Coordinator: ObservableObject {
        // MARK: Stored Properties
        
        @Published var accessToken: String?
        
        public private(set) var text = "Hello, World!"
        
        private var oauth2: OAuth2?
        private var cancellables = Set<AnyCancellable>()
        
        
        // MARK: Initialization
        public init(configuration: OAuth2Configuration) {
            let oauth2 = OAuth2CodeGrant(settings: [
                "client_id": configuration.clientId,
                "client_secret": "",
                "authorize_uri": configuration.authorizeUri,
                "token_uri": configuration.tokenUri,
                "redirect_uris": configuration.redirectUris,
                "scope": configuration.scope,
                "parameters": configuration.customParameters,
                "secret_in_body": false,
                "keychain": true,
                "use_pkce": true,
            ] as OAuth2JSON)
            oauth2.logger = OAuth2DebugLogger(configuration.logLevel)
            oauth2.afterAuthorizeOrFail = { [unowned self] authParameters, error in
                self.accessToken = oauth2.accessToken
            }
            self.oauth2 = oauth2
        }
        
        // MARK: Methods
        public func checkToken() {
            if let hasUnexpiredAccessToken = self.oauth2?.hasUnexpiredAccessToken(), hasUnexpiredAccessToken {
                self.oauth2?.doRefreshToken(callback: { [unowned self] authParameters, error in
                    self.accessToken = self.oauth2?.accessToken
                })
            }
        }
        
        public func setupWhenUIIsReady() {
            if let oauth2 = self.oauth2 {
    #if os(iOS)
                let rootViewController = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController
    #elseif os(macOS)
                let rootViewController = NSApplication.shared.windows.first?.windowController
    #endif
                
                if rootViewController != nil {
                    oauth2.authConfig.authorizeEmbedded = true
                    oauth2.authConfig.authorizeContext = rootViewController
                    oauth2.authConfig.ui.useSafariView = false
                    oauth2.authConfig.ui.useAuthenticationSession = true
                    oauth2.authConfig.ui.prefersEphemeralWebBrowserSession = true
                }
            }
        }
        
        public func authorize() {
            if let oauth2 = self.oauth2 {
                oauth2.authorize() { authParameters, error in
                    if let error = error {
                        print("Authorization was canceled or went wrong: \(error).")
                    }
                }
            }
        }
        
        public func unauthorize() {
            signOutTokenFromServer()
            removeCookies()
            self.oauth2?.abortAuthorization()
            self.oauth2?.forgetTokens()
        }
        
        private func signOutTokenFromServer() {
            if let oauth2 = self.oauth2 {
                if let signOutUriString = oauth2.clientConfig.customParameters?.first(where: { $0.key == "signOutUri" })?.value {
                    var req = oauth2.request(forURL: URL(string: signOutUriString)!, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData)
                    req.httpMethod = "POST"
                    let task = oauth2.session.dataTask(with: req) { data, response, error in
                        if let response = response as? HTTPURLResponse, response.statusCode == 204 {
                            print("Sign out from server.")
                        } else if let error = error {
                            print(error)
                        }
                    }
                    task.resume()
                }
            }
        }
        
        private func removeCookies() {
            HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)
            print("All cookies deleted.")
            
            WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
                records.forEach { record in
                    WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
                    print("Cookie \(record) deleted.")
                }
            }
        }
    }
    
    
    
    opened by rushairer 1
  • "invalid_grant" error in google authentication only for iOS 16 devices.

    Hello, For iOS 16 device getting "invalid_grant"(Bad Request) error. The same code is working for the iOS 15.1 version.

    Code setup: self.oauth2 = OAuth2CodeGrant(settings: [ "client_id": “XXXXX”, "authorize_uri": "https://accounts.google.com/o/oauth2/v2/auth", "token_uri": "https://www.googleapis.com/oauth2/v4/token", "redirect_uris": [“XXXX"], "scope": "https://www.googleapis.com/auth/youtube.upload", "use_keychain": true, "keychain_access_mode": kSecAttrAccessibleAfterFirstUnlock, "keychain_account_for_tokens": “XXXXXX”, ] as OAuth2JSON)

    self.oauth2.authorize { result, error in print("error",error?.localizedDescription) // The operation couldn’t be completed. (Base.OAuth2Error error 13.) }

    Testing version: p2/OAuth2: Master branch's latest version, Realdevices: iPhone 11 (iOS16) - not working, iphone 11(iOS 15.1) - working.

    opened by archana211 0
  • Xcode 14 Beta will get

    Xcode 14 Beta will get "This method should not be called on the main thread as it may lead to UI unresponsiveness." warning

    Run as a Mac app in macOS 13 beta with Xcode 14 beta, I got this warning infomation.

    This method should not be called on the main thread as it may lead to UI unresponsiveness.
    
    2022-09-21_00-05-34
    
    func authorize() {
            
    #if os(iOS)
            let rootViewController = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController
    #elseif os(macOS)
            let rootViewController = NSApplication.shared.keyWindow?.windowController
    #endif
            
            if rootViewController != nil {
                self.oauth.authConfig.authorizeEmbedded = true
                self.oauth.authConfig.authorizeContext = rootViewController
                self.oauth.authConfig.ui.useSafariView = false
                self.oauth.authConfig.ui.useAuthenticationSession = true
                self.oauth.authConfig.ui.prefersEphemeralWebBrowserSession = true
            }
            
            self.oauth.authorize() { [unowned self] authParameters, error in
                if let error = error {
                    print("Authorization was canceled or went wrong: \(error).")
                } else {
                    fetchViewer()
                }
            }
        }
    
    
    opened by rushairer 0
  • How to wait oauth callback for triggering handleRedirectURL

    How to wait oauth callback for triggering handleRedirectURL

    Now, I've another issue: I've those methods

    func runOauth(){
            self.loadingLabel.isHidden=true
            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            appDelegate.oauth2!.afterAuthorizeOrFail = self.callBackOAuth
    
            var url:URL?
            do{
                //the url for authorizing the user, kronos://oauth/callback" is called after the OAuth finish
                
                url = try appDelegate.oauth2!.authorizeURL(withRedirect:"kronos://oauth/callback", scope: "auth",params: ["tg":"addon/kronos/main","idx":"login.OAuth","formId":"iOS"])
                do{
                    let authorizer = appDelegate.oauth2!.authorizer as! OAuth2Authorizer
                    //launch OAuth in embeded view "SafariVC"
                    print("Safari embeded" + url!.absoluteString)
                    
                    safariVC = try authorizer.authorizeSafariEmbedded(from: self,at: url!)
                    
                }catch let error {
                    DispatchQueue.main.async {
                        print("ERROR authorizing\(error)")
                        //self.runOauth()
                    }
                }
            }catch let error {
                DispatchQueue.main.async {
                    print("ERROR creating OAuth URL \(error)")
                    //self.runOauth()
                }
            }
        }
    
    func callBackOAuth(authParameters:OAuth2JSON!, error: OAuth2Error!){
            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            print("Callback")
            if (error ==  nil && appDelegate.oauth2!.accessToken != nil){//OAuth succeed in
                
                //we store the token and its experation date in keychain
                print("OAuth succeed")
                self.keychain!.set(appDelegate.oauth2!.accessToken!,forKey:"Token")
                //self.keychain!.set(appDelegate.oauth2!.refreshToken!,forKey:"RefreshToken")
                let formatter = DateFormatter()
                formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
                let myString = formatter.string(from: appDelegate.oauth2!.accessTokenExpiry!)
                self.keychain!.set(myString,forKey:"ExpiryDate")
                self.loadingLabel.isHidden=false
                
                appDelegate.reloadView()
            }else if (error !=  nil){//OAUth failed
                print("OAuth error \(String(describing: error))")
            }else{//Another error
                print("Cannot login")
                self.showMessage(msg: "Login error", title: "Error")
                self.runOauth()
            }
        }
    

    and in AppDelegate

    func application(_ app: UIApplication,
                      open url: URL,
                      options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
            // you should probably first check if this is the callback being opened
                // if your oauth2 instance lives somewhere else, adapt accordingly
            let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
            let site=components?.host
            print("Application")
            if site == "oauth"{//OAuth terminated
                if components?.path == "/callback" {
                    let viewController = self.window?.rootViewController as! ViewController
                    print("oauth")
                    self.oauth2!.handleRedirectURL(url)
                    viewController.hideSafariView()
                }
            }
    
            return true
        }
    

    My issue is that as I trigger runOauth like that it happens that application is called before callBackOAuth so after oauth viewDidAppear is recalled but with keychain token not set, so here is a way to "wait" in application that token is not nil

    override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            //initialize OAuth2 config parameters
            appDelegate.oauth2 = OAuth2CodeGrant(settings: OAuthParams  )
            appDelegate.oauth2!.authConfig.authorizeContext = KronosWebsite?.window
            appDelegate.oauth2!.useKeychain = false
            appDelegate.oauth2!.authConfig.authorizeEmbeddedAutoDismiss = true
    
            appDelegate.oauth2!.logger = OAuth2DebugLogger(.debug)
            appDelegate.oauth2!.afterAuthorizeOrFail = self.callBackOAuth
            appDelegate.oauth2!.verbose = true
            
            
            
            //try to load the Token from keychain
            let token=self.keychain!.get("Token")
            
            if(token == nil){//no token found, we launch the OAuth
                print("no token yet")
                runOauth()
            } 
    

    EDIT: I've tried to use a DispatchGroup with no success:

    groupOauth.enter() in runOauth

    groupOauth.leave() in callBackOAuth

    and in AppDelegate::application

     viewController.groupOauth.notify(queue: DispatchQueue.main) {
        self.oauth2!.handleRedirectURL(url)
        viewController.hideSafariView()
    }
    
    opened by cgkronos 1
  • Clean OAuth2 token

    Clean OAuth2 token

    Hello, In swift I'm calling successfully a callback URL which revoke a token after the user is logout, and right after I call this to enable re-logging

    func runOauth(){
        self.loadingLabel.isHidden=true
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
    
        appDelegate.oauth2!.logger = OAuth2DebugLogger(.debug)
        
        //code executed when OAuth have finished
        appDelegate.oauth2!.afterAuthorizeOrFail = self.callBackOAuth
        
    
        var url:URL?
        do{
            //the url for authorizing the user, kronos://oauth/callback" is called after the OAuth finish
            url = try appDelegate.oauth2!.authorizeURL(withRedirect:"kronos://oauth/callback", scope: "auth",params: ["tg":"addon/kronos/main","idx":"login.OAuth","formId":"iOS"])
            do{
                let authorizer = appDelegate.oauth2!.authorizer as! OAuth2Authorizer
                //launch OAuth in embeded view "SafariVC"
                print("Safari embeded")
                safariVC = try authorizer.authorizeSafariEmbedded(from: self,at: url!)
                
            }catch let error {
                DispatchQueue.main.async {
                    print("ERROR authorizing\(error)")
                    //self.runOauth()
                }
            }
        }catch let error {
            DispatchQueue.main.async {
                print("ERROR creating OAuth URL \(error)")
                //self.runOauth()
            }
        }
    }
    

    But it re-log the user automatically when loading logging page (I see briefly safariVC but it is dismissed almost instantly), strangely the first logoff works well but if I relog I cannot sign off anymore and I have that line in the console when it relog [Debug] OAuth2: Did exchange code for access [true] and refresh [true] tokens In DB the previous token is deleted at the revocation and a new one is created, so I don't know how the user can be relogged without crendentials asked

    opened by cgkronos 7
Releases(5.2.0)
Owner
Pascal Pfiffner
Pascal Pfiffner
MQTT for iOS and macOS written with Swift

CocoaMQTT MQTT v3.1.1 client library for iOS/macOS/tvOS written with Swift 5 Build Build with Xcode 11.1 / Swift 5.1 Installation CocoaPods Install us

EMQ X MQTT Broker 1.4k Jan 1, 2023
A delightful networking framework for iOS, macOS, watchOS, and tvOS.

AFNetworking is a delightful networking library for iOS, macOS, watchOS, and tvOS. It's built on top of the Foundation URL Loading System, extending t

AFNetworking 33.3k Jan 5, 2023
iOS Network monitor/interceptor framework written in Swift

NetShears NetShears is a Network interceptor framework written in Swift. NetShears adds a Request interceptor mechanisms to be able to modify the HTTP

Divar 119 Dec 21, 2022
A peer to peer framework for OS X, iOS and watchOS 2 that presents a similar interface to the MultipeerConnectivity framework

This repository is a peer to peer framework for OS X, iOS and watchOS 2 that presents a similar interface to the MultipeerConnectivity framework (which is iOS only) that lets you connect any 2 devices from any platform. This framework works with peer to peer networks like bluetooth and ad hoc wifi networks when available it also falls back onto using a wifi router when necessary. It is built on top of CFNetwork and NSNetService. It uses the newest Swift 2's features like error handling and protocol extensions.

Manav Gabhawala 93 Aug 2, 2022
DispatchSource based socket framework written in pure Swift

SwiftDSSocket Overview SwiftDSSocket is a purely swift based asynchronous socket framework built atop DispatchSource. Function signatures are pretty m

Yi Huang 65 Nov 15, 2022
Lightweight, flexible HTTP server framework written in Swift

Hummingbird Lightweight, flexible server framework written in Swift. Hummingbird consists of three main components, the core HTTP server, a minimal we

Hummingbird 245 Dec 30, 2022
Server-side Swift. The Perfect core toolset and framework for Swift Developers. (For mobile back-end development, website and API development, and more…)

Perfect: Server-Side Swift 简体中文 Perfect: Server-Side Swift Perfect is a complete and powerful toolbox, framework, and application server for Linux, iO

PerfectlySoft Inc. 13.9k Jan 6, 2023
🌏 A zero-dependency networking solution for building modern and secure iOS, watchOS, macOS and tvOS applications.

A zero-dependency networking solution for building modern and secure iOS, watchOS, macOS and tvOS applications. ?? TermiNetwork was tested in a produc

Bill Panagiotopoulos 90 Dec 17, 2022
Bonjour networking for discovery and connection between iOS, macOS and tvOS devices.

Merhaba Bonjour networking for discovery and connection between iOS, macOS and tvOS devices. Features Creating Service Start & Stop Service Stop Brows

Abdullah Selek 67 Dec 5, 2022
ZeroMQ Swift Bindings for iOS, macOS, tvOS and watchOS

SwiftyZeroMQ - ZeroMQ Swift Bindings for iOS, macOS, tvOS and watchOS This library provides easy-to-use iOS, macOS, tvOS and watchOS Swift bindings fo

Ahmad M. Zawawi 60 Sep 15, 2022
Shawn Frank 2 Aug 31, 2022
RestKit is a framework for consuming and modeling RESTful web resources on iOS and OS X

RestKit RestKit is a modern Objective-C framework for implementing RESTful web services clients on iOS and Mac OS X. It provides a powerful object map

The RestKit Project 10.2k Dec 29, 2022
Lightweight Networking and Parsing framework made for iOS, Mac, WatchOS and tvOS.

NetworkKit A lightweight iOS, Mac and Watch OS framework that makes networking and parsing super simple. Uses the open-sourced JSONHelper with functio

Alex Telek 30 Nov 19, 2022
A networking library for iOS, macOS, watchOS and tvOS

Thunder Request Thunder Request is a Framework used to simplify making http and https web requests. Installation Setting up your app to use ThunderBas

3 SIDED CUBE 16 Nov 19, 2022
Official ProtonVPN iOS and macOS app

ProtonVPN for iOS and macOS Copyright (c) 2021 Proton Technologies AG Dependencies This app uses CocoaPods for most dependencies. Everything is inside

ProtonVPN 121 Dec 20, 2022
Kiwix for offline access on iOS and macOS

Kiwix for iOS & macOS This is the home for Kiwix apps on iOS and macOS. Mobile app for iPads & iPhones Download the iOS mobile app on iTunes App Store

Kiwix 299 Dec 21, 2022
Passepartout is a non-official, user-friendly OpenVPN® client for iOS and macOS.

Passepartout Passepartout is a non-official, user-friendly OpenVPN® client for iOS and macOS. Overview All profiles in one place Passepartout lets you

Passepartout 523 Dec 27, 2022
Lightweight library for web server applications in Swift on macOS and Linux powered by coroutines.

Why Zewo? • Support • Community • Contributing Zewo Zewo is a lightweight library for web applications in Swift. What sets Zewo apart? Zewo is not a w

Zewo 1.9k Dec 22, 2022
Super lightweight async HTTP server library in pure Swift runs in iOS / MacOS / Linux

Embassy Super lightweight async HTTP server in pure Swift. Please read: Embedded web server for iOS UI testing. See also: Our lightweight web framewor

Envoy 540 Dec 15, 2022