403 with initial API call when using Amazon Cognito iOS - ios

I'm using Amazon Cognito for authentication and AWS iOS SDK v. 2.6.11 in my project. My app has the following flow on the main view: Get session, then make an API call using subclass of AWSAPIGateway class.
The issue here is that after successfully authenticating with Amazon Cognito, the API call response code is 403.
After stopping the app and then running it again (now the user is already authenticated) the response status code from the API is 200.
This is the message in responseData I get from the API call with 403 response:
"Message":"User: arn:aws:sts::############:assumed-role/####_unauth_MOBILEHUB_##########/CognitoIdentityCredentials is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:############:********####:##########/Development/POST/my-api-endpoint
(identifiers replaced with # characters)
It seems that the API calls are unauthorized. Is there a way to make those API calls authorized after an successful authentication?
This is the authentication code in my initial UIViewController:
let user = pool.currentUser() ?? pool.getUser()
user.getSession("myUsername", password: "myPassword", validationData: nil).continueOnSuccessWith { sessiontask -> Any? in
// i've left error handling out of this example code
let request = AWSAPIGatewayRequest(httpMethod: "POST",
urlString: "/my-api-endpoint",
queryParameters: nil,
headerParameters: nil,
httpBody: nil)
let serviceClient = AWSAPI_MY_AUTOGENERATED_Client.default()
return serviceClient.invoke(request).continueOnSuccessWith(block: { (task) -> Any? in
if let result = task.result, result.statusCode == 200 {
// A: all good - Continue
} else {
// B: Handle error (403 etc.)
}
return nil
})
This is how my AppDelegate looks like:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let pool = AWSCognitoIdentityUserPool.default()
let credentialsProvider = AWSMobileClient.sharedInstance().getCredentialsProvider()
let configuration = AWSServiceConfiguration(
region: .EUCentral1,
credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
// keeping reference to the pool and the credentials provider
self.pool = pool
self.credentialsProvider = credentialsProvider
window = UIWindow(frame: UIScreen.main.bounds)
let rootViewController = MyInitialViewController()
window!.rootViewController = rootViewController
window!.makeKeyAndVisible()
return AWSMobileClient.sharedInstance().interceptApplication(application, didFinishLaunchingWithOptions: launchOptions)
}

Related

How to see access token when using a sessionmanager?

I'm using the Spotify API, and followed the instructions for their iOS SDK setup. This involved abstracting away/delegating away most of the work to the SPTSession/SPTSessionManager. But now that everything is abstracted away, how can I get my access token so that I can do "manual" calls (using URL, URLSession, URLRequest, etc classes in Swift)?
The instructions for set up had me include a lazy var sessionmanager (an instance of SPTSessionManager) defined via closure that manages my SPTSession, and I know an SPTSession has members access_token and refresh_token. However, I can't seem to access them outside of my Appdelegate because sessionmanager is a lazy var in AppDelegate. I tried making global variables for the tokens outside of appdelegate, but I can't seem to find a way to actually set the values since sessionmanager is completely abstracted away. Even when I'm in the AppDelegate, if I try doing sessionmanager.session?.access_token it sends me into an infinite loop, which I assume is because sessionmanager is a lazy variable so every time I try to get it, the closure just reevaluates?
I'm not sure what's going on. Also, I'm not very familiar with stackoverflow etiquette so please let me know if there's anything I should do differently!
class AppDelegate: UIResponder, UIApplicationDelegate, SPTSessionManagerDelegate {
// Default appdelegate methods and other irrelevant code
// implement session delegate
func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) {
isLoggedIn = true
authToken = session.accessToken
refreshToken = session.refreshToken
print("success", session)
}
func sessionManager(manager: SPTSessionManager, didFailWith error: Error) {
print("fail", error)
}
func sessionManager(manager: SPTSessionManager, didRenew session: SPTSession) {
authToken = session.accessToken
print("renewed", session)
}
let SpotifyClientID = "b29fa2b4649e4bc697ecbf6721edaa39"
let SpotifyRedirectURL = URL(string: "spotify-ios-quick-start://spotify-login-callback")!
lazy var configuration = SPTConfiguration(
clientID: SpotifyClientID,
redirectURL: SpotifyRedirectURL
)
// Setup token swap via glitch
lazy var sessionManager: SPTSessionManager = {
if let tokenSwapURL = URL(string: "https://spotify-token-swap.glitch.me/api/token"),
let tokenRefreshURL = URL(string: "https://spotify-token-swap.glitch.me/api/refresh_token") {
self.configuration.tokenSwapURL = tokenSwapURL
self.configuration.tokenRefreshURL = tokenRefreshURL
self.configuration.playURI = ""
}
let manager = SPTSessionManager(configuration: self.configuration, delegate: self)
return manager
}()
// Request to login with Spotify. Called from a different file.
func requestSpotify() {
let requestedScopes: SPTScope = [.userTopRead, .playlistModifyPublic]
sessionManager.initiateSession(with: requestedScopes, options: .default)
}
// Configure auth callback
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
self.sessionManager.application(app, open: url, options: options)
return true
}
}
Edit: As a separate issue, I can't seem to actually open the login flow cause I have an "invalid client", even though I've whitelisted my redirect uri and I'm sure my client id is correct?

Spotify token swap : unsupported URL error

I'm trying to implement the token swap and refresh for the Spotify sdk. I'm using the Heroku app as a server. Here is my code.
In my Sign-in view controller:
var auth = SPTAuth.defaultInstance()!
var session: SPTSession!
var player: SPTAudioStreamingController?
SPTAuth.defaultInstance().clientID = "********************************"
SPTAuth.defaultInstance().redirectURL = URL(string: "viraj-project2://callback" )
SPTAuth.defaultInstance().tokenSwapURL = URL(string: "https://viraj-project2.herokuapp.com/v1/swap")
SPTAuth.defaultInstance().tokenRefreshURL = URL(string: "https://viraj-project2.herokuapp.com/v1/refresh")
SPTAuth.defaultInstance().requestedScopes = [SPTAuthStreamingScope, SPTAuthPlaylistReadPrivateScope, SPTAuthPlaylistModifyPublicScope, SPTAuthPlaylistModifyPrivateScope, SPTAuthUserLibraryReadScope, SPTAuthUserLibraryModifyScope]
loginUrl = SPTAuth.defaultInstance().spotifyWebAuthenticationURL()
#IBAction func signIn(_ sender: Any) {
self.performSegue(withIdentifier: "toNewsFeed", sender: self)
if SPTAuth.supportsApplicationAuthentication() {
UIApplication.shared.open(loginUrl!, options: [:], completionHandler: nil)
} else {
if UIApplication.shared.openURL(loginUrl!) {
if auth.canHandle(auth.redirectURL) {
// To do - build in error handling
}
}
}
}
and in my AppDelegate
class AppDelegate: UIResponder, UIApplicationDelegate{
var window: UIWindow?
var auth = SPTAuth()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
auth.redirectURL = URL(string: "viraj-project2")
auth.sessionUserDefaultsKey = "current session"
// Override point for customization after application launch.
return true
}
func application(_ application: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
// 2- check if app can handle redirect URL
if auth.canHandle(auth.redirectURL) {
// 3 - handle callback in closure
print (url)
auth.handleAuthCallback(withTriggeredAuthURL: url, callback: { (error, session) in
// 4- handle error
if error != nil {
print(error)
print("error!")
}
NotificationCenter.default.post(name: Notification.Name(rawValue: "loggedinperformsegue"), object: nil)
// 5- Add session to User Defaults
let userDefaults = UserDefaults.standard
let sessionData = NSKeyedArchiver.archivedData(withRootObject: session!)
userDefaults.set(sessionData, forKey: "SpotifySession")
userDefaults.synchronize()
// 6 - Tell notification center login is successful
NotificationCenter.default.post(name: Notification.Name(rawValue: "loginSuccessfull"), object: nil)
})
return true
}
return false
}
The authentication works fine without adding the swap and refresh URLs. But whenever I run it with the two URLS added to the SPTAuth.defaultInstance() I get a unsupported URL error like so:
viraj-project2://callback/?code=AQDhKLE9s5GQGITEn**********5_y9aKZM6_nSlzA
2018-09-14 16:55:08.258063-0400 Project2[17606:3197471] NSURLConnection finished with error - code -1002
Optional(Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL, NSUnderlyingError=0x109385130 {Error Domain=kCFErrorDomainCFNetwork Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL}}})
error!
Can someone help?
Update on further digging:
When I include add the tokenSwapURL and tokenRefreshURL to my default instance the loginUrl formed looks like so
"https://accounts.spotify.com/authorize?nolinks=true&nosignup=true&response_type=code&scope=streaming%20playlist-read-private%20playlist-modify-public%20playlist-modify-private%20user-library-read%20user-library-modify&utm_source=spotify-sdk&utm_medium=ios-sdk&utm_campaign=ios-sdk&redirect_uri=viraj-project2%3A%2F%2Fcallback&show_dialog=true&client_id=****************"
The response type is "code" which as per the documentation is the right one according to Authorization Flow. But this is what returns the unsupported URL error.
Without the swap and refresh URLs the loginURL looks the same except that the response_type says "token". And this works fine. Is that right though? Doesn't response_type = token mean implicit grant flow?
https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow

Trying to use Cognito to download files from S3

I am using AWS S3 as a backend for sound file storage in an iOS app. I can upload sound files to the bucket as I wish, but I am having trouble to make the download work.
I was first hoping to make things work using PFFile, but since I did not succeed as expected, I did some research and had the impression that using Cognito was the way to go. Since it is my firstime to use it, I may well be missing some important part.
Here is the application:didFinishLaunchingWithOptions method.
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
..........
// For the AWSS3 configuration:
let poolID = "us-east-1:123.........",
credentialProvider = AWSCognitoCredentialsProvider(regionType: .USEast1,
identityPoolId: poolID)
let awsConfig = AWSServiceConfiguration(region: .APNortheast1,
credentialsProvider: credentialProvider)
AWSServiceManager.default().defaultServiceConfiguration = awsConfig
return true
}
At some point I have this code executed:
transferManager = AWSS3TransferManager.default()
For downloading the sound file, this code is run:
let soundFileName = ....,
downloadFileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(soundFileName),
downloadRequest = AWSS3TransferManagerDownloadRequest()
downloadRequest?.bucket = "theappbucket"
downloadRequest?.key = soundFileName
downloadRequest?.downloadingFileURL = downloadFileURL
transferManager.download(downloadRequest!).continueWith(
executor: AWSExecutor.mainThread(),
block: {
(task:AWSTask<AnyObject>) -> Any? in
if let error = task.error {
print("Error in \(#function)\n" + error.localizedDescription)
print("Error in \(#function)\n\(error)")
return nil
}
.....
});
Here I get this error:
The operation couldn’t be completed. (com.amazonaws.AWSServiceErrorDomain error 11.)
UserInfo={HostId=ND....../FY=, Message=Access Denied, Code=AccessDenied, RequestId=EAC.....}
Having some doubts, I have also tried using .USEast1 instead of .APNortheast1, in which case the error becomes:
The operation couldn’t be completed. (com.amazonaws.AWSS3ErrorDomain error 0.)
But since my bucket is set in Asia Pacific (Tokyo) I presume the correct setting is .APNortheast1.
Can someone point out some problem in what I am doing?

How to redirect back to my iOS app after logging in on FitBit login page?

I am developing a very basic iOS app with Swift. Just to read the heart rate data. I am using SFSafariViewController. As known, I first need to register my app on dev.fitbit.com. The registration form requires a callback URL to be entered.
After logging in successfully, FitBit always redirects me back to that entered callback URL. What should I do/code/configure to be able to redirect user back to my iOS app after logging in successfully?
What you need to do is to add the application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool function to your AppDelegate and then create an url scheme for your application as an identifier. To create an url scheme go to your application target > Info > URL Types (at the bottom). Then just add the following in your AppDelegate:
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
DispatchQueue.main.async {
// Conctrol so that we´re coming from the right application
if (url.scheme == "The url scheme that you created"){
// Navigate to the viewController you want
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "WebView") as! WebViewController
self.window?.rootViewController!.present(controller, animated: true, completion: { () -> Void in
})
}
}
return true
}
Try using below 3 steps in your application.
You are using "fitbit", so I am considering your are using OAuth2.0 for login and get access-token.
Step 1 : Set up you URL Schemes.
Step 2 : In your AppDelegate class
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
if (url.host == "oauth-swift") {
OAuthSwift.handle(url: url)
}
return true
}
In Above function what we did is, we check the URL comes to handleOpenUrl method, and check weather it is the right url call back is coming or not.
Step 3 : Set the proper call back URL in you OAuth Handler.
oauthswift = OAuth2Swift(
consumerKey: "********",
consumerSecret: "********",
authorizeUrl: "your authorisation url",
responseType: "token"
)
let handle = oauthswift.authorize(
withCallbackURL: URL(string: "oauth-swift://oauth-callback/fitbit")!,
scope: "your application scope", state:"state",
success: { credential, response, parameters in
print(credential.oauth_token)
},
failure: { error in
print(error.localizedDescription)
}
)
In above step, we set the call back url starting with "oauth-swift:", so it will be work as a host of your call back url.
Image and Code Courtesy : I have tried to explain a solution of your
problem in easy words. And all information of this answers are
originally documented and explained on this URL :
https://github.com/OAuthSwift/OAuthSwift

AWS ExpiredTokenException after app relaunch

I'm building an iOS (Swift) app using AWS as the backend with Developer Authenticated Identities. Everything works fine until I close the app, leave it for a while and then relaunch. In this scenario I often, but not always, receive ExpiredTokenException errors when trying to retrieve data from AWS.
Here is my code:
class DeveloperAuthenticatedIdentityProvider: AWSAbstractCognitoIdentityProvider {
var _token: String!
var _logins: [ NSObject : AnyObject ]!
override var token: String {
get {
return _token
}
}
override var logins: [ NSObject : AnyObject ]! {
get {
return _logins
}
set {
_logins = newValue
}
}
override func getIdentityId() -> AWSTask! {
if self.identityId != nil {
return AWSTask(result: self.identityId)
} else {
return AWSTask(result: nil).continueWithBlock({ (task) -> AnyObject! in
if self.identityId == nil {
return self.refresh()
}
return AWSTask(result: self.identityId)
})
}
}
override func refresh() -> AWSTask! {
let apiUrl = "https://url-goes-here" // call my server to retrieve an OpenIdToken
request.GET(apiUrl, parameters: nil, progress: nil,
success: {
(task: NSURLSessionDataTask, response: AnyObject?) -> Void in
let tmp = NSMutableDictionary()
tmp.setObject("temp", forKey: "ExampleApp")
self.logins = tmp as [ NSObject : AnyObject ]
let jsonDictionary = response as! NSDictionary
self.identityId = jsonDictionary["identityId"] as! String
self._token = jsonDictionary["token"] as! String
awstask.setResult(response)
},
failure: {
(task: NSURLSessionDataTask?, error: NSError) -> Void in
awstask.setError(error)
}
)
return awstask.task
}
}
And in the AppDelegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let identityProvider = DeveloperAuthenticatedIdentityProvider()
// set default service configuration
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: cognitoRegion, identityProvider: identityProvider, unauthRoleArn: unauthRole, authRoleArn: authRole)
let configuration = AWSServiceConfiguration(region: defaultServiceRegion, credentialsProvider: credentialsProvider)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration
// set service configuration for S3 (my bucket is located in a different region to my Cognito and Lambda service)
let credentialsProviderForS3 = AWSCognitoCredentialsProvider(regionType: cognitoRegion, identityProvider: identityProvider, unauthRoleArn: unauthRole, authRoleArn: unauthRole)
let awsConfigurationForS3 = AWSServiceConfiguration(region: s3ServiceRegion, credentialsProvider: credentialsProviderForS3)
AWSS3TransferUtility.registerS3TransferUtilityWithConfiguration(awsConfigurationForS3, forKey: "S3")
return true
}
This post suggests that the Cognito token has expired and it is up to the developer to manually refresh. This seems overly complex as it would require setting a timer to refresh regularly, handling app closures and relaunches and handling AWS requests that occur while the refresh is taking place. Is there a simpler way? For example, is it possible to have the AWS SDK automatically call refresh whenever it attempts to query the server using an expired token?
Any help would be appreciated. I'm using version 2.3.5 of the AWS SDK for iOS.
The AWS Mobile SDK for iOS 2.4.x has a new protocol called AWSIdentityProviderManager. It has the following method:
/**
* Each entry in logins represents a single login with an identity provider.
* The key is the domain of the login provider (e.g. 'graph.facebook.com') and the value is the
* OAuth/OpenId Connect token that results from an authentication with that login provider.
*/
- (AWSTask<NSDictionary<NSString *, NSString *> *> *)logins;
The responsibility of an object conforming to this protocol is to return a valid logins dictionary whenever it is requested. Because this method is asynchronous, you can make networking calls in it if the cached token is expired. The implementation is up to you, but in many cases, AWSIdentityProviderManager manages multiple AWSIdentityProviders, aggregates them and return the logins dictionary.
Unfortunately developers refreshing the token is the only way.
I agree that it would be simpler for app developers if AWS SDK handled this but the way CrdentialsProvider is designed is supposed to be generic for all providers. For example, if someone wants to use Facebook as provider then AWS SDK will not be able to handle the refresh on its own and developer will have t handle that in his app. Keeping the refresh flow out of the SDK gives us the capability to keep the CredentialsProvider generic.

Resources