Callback URL not calling completionHandler - ios

I’m trying to use ASWebAuthentication to facilitate the authentication process with Oauth 1.0. After the
user enters their credentials and approves my application, oauth provider uses the redirect_url I passed in, com.me.appName:/returnToApp
and the Safari window looks something like this:
Here's my code:
func getAuthTokenWithWebLogin(context: ASWebAuthenticationPresentationContextProviding) {
let callbackUrlScheme = “scheme:/returnToApp"
let authURL = URL(string: Constants.authURL)
guard authURL != nil else{return}
let webAuthSession = ASWebAuthenticationSession.init(url: authURL!, callbackURLScheme: callbackUrlScheme, completionHandler: { (callBack:URL?, error:Error?) in
// handle auth response
guard error == nil, let successURL = callBack else {
return
}
let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
// Do what you now that you've got the token, or use the callBack URL
print(oauthToken ?? "No OAuth Token")
})
// New in iOS 13
webAuthSession.presentationContextProvider = context
// Kick it off
webAuthSession.start()
}
I don't think it's an issue with ASWebAuthentication, since I've had the same problem when I tried using third party library OauthSwift

My bad; I didn't realize that the callbackURL must be in myApp:// format, while I had mine as myApp:/ (single slash) thinking this would also work.

Related

How to implement basic web authentication in SwiftUI App for a specific web service that does not provide an API?

I will get straight to the point. Please excuse my english
So there is a website (we can call it X) which offers a service to query and fetch documents etc after logging in with your username / password
I am trying to figure out a way to, basically, provide the same service except natively through the app. The user of the app will still have to enter their username and password and then I make a request to the website and "log" in and then provide the same interface the website does after logging in but the app will be able to save the login information and be able to have some other features that would be beneficial wrt to the documents that it then fetches.
The website does not offer an api (atleast that I know of) I am struggling to figure out how to send the URL request with the username and password.
First I read about using ASAuthenticationServices but that led me to reading that you need the callback url which did not work for me (1. because I can't setup the callback url through the API and 2. because my custom app callback url did not fire)
Then I tried to use a WKWebView to embed a browser and a try to catch the details after the user logged in but still no success.
I have also read that JWT might be the solution considering that it is only a single server authentication needed
This is my code so far, I have stripped it down to basically just show the request I'm making
If anyone could shed some light as to how to perform a simple web login (to basically embed the web service and be able to use it as an app) I would really appreciate it.
Thanks
So this is where I'm at now. I'm trying to form a URLRequest and just checking whether the signin response works, should I be trying to implement a callback in a custom WKWebView ? I'm more kind of asking as to what method I should be using. Should I be researching more into JWT ? or should I be looking at using custom WKWebViews and trying to catch the callback and save the credentials etc through that or do I need to just deconstruct and send custom URLRequests in order to authenticate? thank you
import Combine
import Foundation
final class SignInDummy: ObservableObject {
#Published var username = ""
#Published var password = ""
#Published var hasError = false
#Published var isSigningIn = false
var canSignIn: Bool {
!username.isEmpty && !password.isEmpty
}
func signIn() {
guard !username.isEmpty && !password.isEmpty else {
return
}
let url = URL(string: "https:// URL to website (copy and pasted url of the login page)")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
let authData = (username + ":" + password).data(using: .utf8)!.base64EncodedString()
request.addValue("Basic \(authData)", forHTTPHeaderField: "Authorization")
isSigningIn = true
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
DispatchQueue.main.async {
if error != nil || (response as! HTTPURLResponse).statusCode != 200 {
self?.hasError = true
} else if let data = data {
do {
let signInResponse = try JSONDecoder().decode(SignInResponse.self, from: data)
print(signInResponse)
// TODO: Cache Access Token in Keychain
} catch {
print("Unable to Decode Response \(error)")
print("\(response?.mimeType)")
print("\(response.debugDescription)")
}
}
self?.isSigningIn = false
}
}.resume()
}
fileprivate struct SignInResponse: Decodable {
// MARK: - Properties
let accessToken: String
}
}

Refreshing an Access Token p2/OAuth2 iOS

I'm using p2/OAuth2 with Alamofire v4 as explained in documentation here
let sessionManager = SessionManager()
let retrier = OAuth2RetryHandler(oauth2: <# your OAuth2 instance #>)
sessionManager.adapter = retrier
sessionManager.retrier = retrier
self.alamofireManager = sessionManager // you must hold on to this somewhere
// Note that the `validate()` call here is important
sessionManager.request("https://api.github.com/user").validate().responseJSON { response in
debugPrint(response)
}
import Foundation
import OAuth2
import Alamofire
class OAuth2RetryHandler: RequestRetrier, RequestAdapter {
let loader: OAuth2DataLoader
init(oauth2: OAuth2) {
loader = OAuth2DataLoader(oauth2: oauth2)
}
/// Intercept 401 and do an OAuth2 authorization.
public func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
if let response = request.task?.response as? HTTPURLResponse, 401 == response.statusCode, let req = request.request {
var dataRequest = OAuth2DataRequest(request: req, callback: { _ in })
dataRequest.context = completion
loader.enqueue(request: dataRequest)
loader.attemptToAuthorize() { authParams, error in
guard error?.asOAuth2Error != .alreadyAuthorizing else {
// Don't dequeue requests if we are waiting for other authorization request
return
}
self.loader.dequeueAndApply() { req in
if let comp = req.context as? RequestRetryCompletion {
comp(nil != authParams, 0.0)
}
}
}
}
else {
completion(false, 0.0) // not a 401, not our problem
}
}
/// Sign the request with the access token.
public func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
guard nil != loader.oauth2.accessToken else {
return urlRequest
}
return try urlRequest.signed(with: loader.oauth2) // "try" added in 3.0.2
}
}
Everything is working fine, but what I want to achieve is to avoid 401 errors by fetching an access token if expired before submitting a request.
Is it possible to achieve this approach ?
Thanks in advance,
Regards,
Well the standard behaviour in a mobile app should be as follows:
Login and get access token + refresh token
Optionally store tokens in secure storage so that logins on every app restart are avoided
Use access token to call the API and handle 401 responses via a token renewal
Use the refresh token to renew the access token when needed
Eventually the refresh token expires and the user has to login again
You can never completely avoid 401 responses from the API, and UIs need to handle this response specially. It is possible as an optimisation to refresh tokens silently in the background, before the 'exp' claim from the current access token is reached.
Out of interest there is an iOS Code Sample of mine that is easy to run, and which allows a kind of testing of the expiry events. This may give you some ideas on how to adapt your own solution.

How to get an auth code using ASWebAuthenticationSession?

I'm trying to obtain an auth code from Stripe's OAuth endpoint using ASWebAuthenticationSession - this event happens after the my Stripe redirect url gets called.
Unfortunately, the authSession's completion handler doesn't call back with a callbackURL. And I need this callbackURL to query the auth code. I've read different articles on this topic but I can't figure out why my implementation doesn't work they way I expect it to.
Here's my code:
class CreateStripeConnectAccountController: UIViewController {
var authSession: ASWebAuthenticationSession!
override func viewDidLoad() {
super.viewDidLoad()
configureAuthSession()
}
private func configureAuthSession() {
let urlString = Constants.URLs.stripeConnectOAuth // Sample URL
guard let url = URL(string: urlString) else { return }
let callbackScheme = "myapp:auth"
authSession = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackScheme, completionHandler: { (callbackURL, error) in
guard error == nil, let successURL = callbackURL else {
print("Nothing")
return
}
let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
print(successURL.absoluteString)
})
authSession.presentationContextProvider = self
authSession.start()
}
}
extension CreateStripeConnectAccountController: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
self.view.window ?? ASPresentationAnchor()
}
}
I believe the issue is that you are giving nil for callbackURLScheme. You need to give a URL scheme that redirects to your app:
See apple's authentication example: https://developer.apple.com/documentation/authenticationservices/authenticating_a_user_through_a_web_service
And here's apple's docs on how to create a custom URL scheme for your app: https://developer.apple.com/documentation/uikit/inter-process_communication/allowing_apps_and_websites_to_link_to_your_content/defining_a_custom_url_scheme_for_your_app.
I know it's old, but anyway.
Make sure, that callbackScheme and scheme that is used in redirect_uri are the same.
Your callbackScheme myapp:auth is incorrect format.
The symbol : cannot be used in the scheme name of a URI.
See the following RFC definition of Scheme.
Scheme names consist of a sequence of characters beginning with a
letter and followed by any combination of letters, digits, plus
("+"), period ("."), or hyphen ("-").
https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
Thefore, revising the callbackURLscheme as myapp-auth or myapp.auth works well.
try setting your callbackScheme = "myapp" to receive callback
and from your server-side it should return "myapp://auth?token=1234"
Hope it helps.

Basecamp3 login issue with Oauth2 in Swift

My app needs to get a basecamp3 login. Hence I used the OAuth2Swift library. But unfortunately, I am unable to receive the token from basecamp even the user has authorized the app.
Below is the screenshot
I have used the following code
func createAuthRequest(){
// create an instance and retain it
let oauthswift = OAuth2Swift(
consumerKey: clientID,
consumerSecret: clientSecret,
authorizeUrl: authURL,
responseType: "token"
)
//oauthswift.authorizeURLHandler = self
oauthswift.authorizeURLHandler = SafariURLHandler(viewController: self, oauthSwift: oauthswift)
let handle = oauthswift.authorize(
withCallbackURL: URL(string: redirectURL)!,
scope: "profile", state:"") { result in //This block of code never executed
switch result {
case .success(let (credential, response, parameters)):
print(credential.oauthToken)
// Do your request
case .failure(let error):
print(error.localizedDescription)
}
}
}
The code inside withCallbackURL never executed even the user has authorized the app.
Any help regarding this is appreciated.
I found the solution the problem was I was using wrong authentication & token URL.
Following URL need to be used. I missed to add web_server in auth/token url and unfortunately Basecamp haven't mentioned the same in their documets.
let authURL = "https://launchpad.37signals.com/authorization/new?type=web_server"
let tokenURL = "https://launchpad.37signals.com/authorization/token?type=web_server"
and redirectURL = com.abc.abc:/oauth2Callback (The same redircturl need to be updated for app under basecamp developer console where com.abc.abc is bundle id of the app)

Athentication problems on iOS when using AppAuth and Okta

I have a simple iOS Swift app loosely based on the AppAuth-iOS example (https://github.com/openid/AppAuth-iOS) as well as Okta OAuth sample (https://github.com/oktadeveloper/okta-openidconnect-appauth-ios). I am not using Service Discovery nor authomatic token aquisition (i.e. not using authStateByPresentingAuthorizationRequest).
My sample works against Azure AD but does not work against Okta. I am able to log in and am authenticated and redirected back to my mobile app (AppDelegate.application()) but then the flow does not return to my OIDAuthorizationService.present() completion block.
Here is some code:
#IBAction func signInButton(_ sender: Any) {
// select idp
switch selectedIdentityProvider! {
case "Azure AD":
selectedAuthConfig = AzureAdAuthConfig()
case "Okta":
selectedAuthConfig = OktaAuthConfig();
default:
return
}
appAuthAuthorize(authConfig: selectedAuthConfig!)
}
func appAuthAuthorize(authConfig: AuthConfig) {
let serviceConfiguration = OIDServiceConfiguration(
authorizationEndpoint: NSURL(string: authConfig.authEndPoint)! as URL,
tokenEndpoint: NSURL(string: authConfig.tokenEndPoint)! as URL)
let request = OIDAuthorizationRequest(configuration: serviceConfiguration, clientId: authConfig.clientId, scopes: authConfig.scope, redirectURL: NSURL(string: authConfig.redirectUri)! as URL, responseType: OIDResponseTypeCode, additionalParameters: nil)
doAppAuthAuthorization(authRequest: request)
}
func doAppAuthAuthorization(authRequest: OIDAuthorizationRequest) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(authRequest, presenting: self, callback: {
(authorizationResponse, error) in
if (authorizationResponse != nil) {
self.authState = OIDAuthState(authorizationResponse: authorizationResponse!)
self.logMessage(message: "Got authorization tokens. Access token: \(String(describing: self.authState?.lastAuthorizationResponse.authorizationCode))")
self.doTokenRequest()
} else {
self.authState = nil
self.logMessage(message: "Authorization error: \(String(describing: error?.localizedDescription))")
}
})
}
I could rewrite the code to use authStateByPresentingAuthorizationRequest() to see if it works but am a bit leery as this code works against Azure AD. Any suggestions?
Update 1
I forgot to mention that I have a working Android/Java example going against the same Okta definitions and working like a charm.
Update 2
I did rewrite the code to use authStateByPresentingAuthorizationRequest() against Okta and am getting the same result (i.e. getting stuck after redirect back to my app). I tested this against Azure AD and it works Ok.
Resolved. I guess the problem was that the redirect URL defined in Okta was mixed case. Android AppAuth implementation does not mind but iOS AppAuth implementation does. Changed redirect URL in Okta to lower case only, changed redirect Uri paramter passed in to lower case only and bing, all works great. Thanks #jmelberg for pointing me in this direction - by debugging resumeAuthorizationFlow(with: url) I was able to see the exact behaviour and why the call returned a False.

Resources