How to get an auth code using ASWebAuthenticationSession? - ios

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.

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
}
}

Detecting a valid web address

I am trying to write some code that lets me both validate that a string is in fact a valid web address to reach a remote server && be able to unwrap it safely into a url for usage.
From what gathered from various posts and Apple's source code:
URLComponents is a structure designed to parse URLs based on RFC 3986 and to construct URLs from their constituent parts.
and based on w3 schools:
A URL is a valid URL if at least one of the following conditions holds:
The URL is a valid URI reference [RFC3986]....
Would this code suffice to detect addresses that reach remote servers on the world wide web?
import Foundation
extension String {
/// Returns `nil` if a valid web address cannot be initialized from self
var url: URL? {
guard
let urlComponents = URLComponents(string: self),
let scheme = urlComponents.scheme,
isWebServerUrl(scheme: scheme),
let url = urlComponents.url
else {
return nil
}
return url
}
/// A web address normally starts with http:// or https:// (regular http protocol or secure http protocol).
private func isWebServerUrl(scheme: String) -> Bool {
(scheme == WebSchemes.http.rawValue || scheme == WebSchemes.https.rawValue)
}
}
Can you provide some feedback on this approach and let me know if there are any optimizations that can be made? or if it's incorrect?
Any and all comments are appreciated.
You could go even simpler and do
import Foundation
extension String {
/// Returns `nil` if a valid web address cannot be initialized from self
var url: URL? {
return URL(string: self)
}
/// A web address normally starts with http:// or https:// (regular http protocol or secure http protocol).
var isWebURL: Bool {
get {
guard let url = self.url else { return false }
return url.scheme == "http" || url.scheme == "https"
}
}
}
Explanation
Initializing a URL using a string will return nil if the string isn't a valid url, so you can get the url by just initing a URL object. Additionally, checking the scheme is super easy because URL has a property scheme that we can check against the required parameters.

Callback URL not calling completionHandler

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.

How to read a parameter in a Vapor middleware without consuming it

I am brand new to the Vapor framework, and am trying to protect multiple routes. Basically, I want to make sure that all routes under /campaigns/:id can only be accessed if the user actually has access to that particular campaign with that ID. So that I can't just enter any ID into the url and access any campaign.
Now, instead of adding logic for this to every single route (already 6 so far), I figured I'd use a middleware for this. This is what I came up with so far, with the help of some friendly folk over at the Vapor Discord:
final class CampaignMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
let user = try request.requireAuthenticated(User.self)
return try request.parameters.next(Campaign.self).flatMap(to: Response.self) { campaign in
guard try campaign.userID == user.requireID() else {
throw Abort(.forbidden, reason: "Campaign doesn't belong to you!")
}
return try next.respond(to: request)
}
}
}
struct CampaignController: RouteCollection {
func boot(router: Router) throws {
let route = router.grouped("campaigns")
let tokenAuthMiddleware = User.tokenAuthMiddleware()
let guardMiddleware = User.guardAuthMiddleware()
let tokenAuthGroup = route.grouped(tokenAuthMiddleware, guardMiddleware)
tokenAuthGroup.get(use: getAllHandler)
tokenAuthGroup.post(CampaignCreateData.self, use: createHandler)
// Everything under /campaigns/:id/*, CampaignMiddleware makes sure that the campaign actually belongs to you
let campaignRoute = tokenAuthGroup.grouped(Campaign.parameter)
let campaignMiddleware = CampaignMiddleware()
let protectedCampaignRoute = campaignRoute.grouped(campaignMiddleware)
protectedCampaignRoute.get(use: getOneHandler)
protectedCampaignRoute.delete(use: deleteHandler)
protectedCampaignRoute.put(use: updateHandler)
// Add /campaigns/:id/entries routes
let entryController = EntryController()
try protectedCampaignRoute.register(collection: entryController)
}
func getAllHandler(_ req: Request) throws -> Future<[Campaign]> {
let user = try req.requireAuthenticated(User.self)
return try user.campaigns.query(on: req).all()
}
func getOneHandler(_ req: Request) throws -> Future<Campaign> {
return try req.parameters.next(Campaign.self)
}
// ...deleted some other route handlers...
}
The problem here is that the middleware is "eating up" the campaign parameter by doing request.parameters.next(Campaign.self). So in getOneHandler, where it also tries to access req.parameters.next(Campaign.self), it fails with error "Insufficient parameters". Which makes sense, since .next actually removes that param from the internal array of parameters.
Now, how can I write a middleware, that uses the parameter, without eating it up? Do I need to use the raw values and query the Campaign model myself? Or can I somehow reset the parameters after using .next? Or is there another better way to deal with model authorization in Vapor 3?
Heeey, it looks like you could get your Campaign from request without dropping it like this
guard let parameter = req.parameters.values.first else {
throw Abort(.forbidden)
}
try Campaign.resolveParameter(parameter.value, on: req)
So your final code may look like
final class CampaignMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
let user = try request.requireAuthenticated(User.self)
guard let parameter = request.parameters.values.first else {
throw Abort(.forbidden)
}
return try Campaign.resolveParameter(parameter.value, on: request).flatMap { campaign in
guard try campaign.userID == user.requireID() else {
throw Abort(.forbidden, reason: "Campaign doesn't belong to you!")
}
return try next.respond(to: request)
}
}
}

Swift URL query string get parameters

I'm using the OAuthSwift library to authenticate users of my app, but something doesn't seem to be working as it throws an error. After some debugging it seems to be going wrong on this line: parameters = url.query!.parametersFromQueryString()
It does have the url query string (set to url.query) but it somehow fails parsing the parametersFromQueryString(). Does someone else have this problem (with this library) and how did you solve it?
The "parametersFromQueryString" function can be found here: https://github.com/dongri/OAuthSwift/blob/1babc0f465144411c0dd9721271f239685ce83a9/OAuthSwift/String%2BOAuthSwift.swift
Create an extension of URL class and paste this code:
import Foundation
extension URL {
public var parametersFromQueryString : [String: String]? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems else { return nil }
return queryItems.reduce(into: [String: String]()) { (result, item) in
result[item.name] = item.value
}
}}
Now you can get the value of this calculated attribute from URL object by simply using:
url.parametersFromQueryString
Have you checked that url.query returns anything? and that it is url decoded before/after calling url.query?
try looking at the answers here:
URL decode in objective-c
If the method is looking for characters such as '?' or '=' and the URL is still encoded, it might not find them
Here is the same code as it should look like in swift:
let URLString = "http://sound17.mp3pk.com/indian/barfi/%5BSongs.PK%5D%20Barfi%20-%2001%20-%20Barfi!.mp3"
let URL = NSURL(string:url)
let filename = URL.path.lastPathComponent.stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
//=> [Songs.PK] Barfi - 01 - Barfi!.mp3

Resources