Determine login type using AWS Cognito during resume session (Swift) - swift

I'm having a hard time trying to figure out how to determine the login type during a resume session using AWS Cognito. My code is based upon the MobileHub sample (below).
I've integrated a name/password mode for user pools (account creation and login) as well as as a Facebook login button which all works perfectly.
I have some logic in my application that needs to behave differently depending on the login type but I can't figure out how to do it.
Anyone done this?
func didFinishLaunching(_ application: UIApplication, withOptions launchOptions: [AnyHashable: Any]?) -> Bool {
print("didFinishLaunching:")
// Register the sign in provider instances with their unique identifier
AWSSignInManager.sharedInstance().register(signInProvider: AWSFacebookSignInProvider.sharedInstance())
AWSIdentityProfileManager.sharedInstance().register(FacebookIdentityProfile.sharedInstance(), forProviderKey: AWSFacebookSignInProvider.sharedInstance().identityProviderName)
AWSSignInManager.sharedInstance().register(signInProvider: AWSCognitoUserPoolsSignInProvider.sharedInstance())
AWSIdentityProfileManager.sharedInstance().register(UserPoolsIdentityProfile.sharedInstance(), forProviderKey: AWSCognitoUserPoolsSignInProvider.sharedInstance().identityProviderName)
setupAPIGateway()
setupS3()
let didFinishLaunching: Bool = AWSSignInManager.sharedInstance().interceptApplication(application, didFinishLaunchingWithOptions: launchOptions)
if (!isInitialized) {
AWSSignInManager.sharedInstance().resumeSession(completionHandler: { (result: Any?, authState: AWSIdentityManagerAuthState, error: Error?) in
print("didFinishLaunching Result: \(String(describing: result)) AuthState: \(authState) \n Error:\(String(describing: error))")
if authState == .authenticated {
// Facebook or Cognito???
AWSCognitoUserAuthHelper.getCurrentUserAttribute(name: "sub", completionHandler: { (userid) in
// we need to fetch the user
ObjectManager.instance.getUser(userid: userid, completionHandler: { (user) in
ObjectManager.instance.setCurrentUser(user: user)
})
})
}
}) // If you get an EXC_BAD_ACCESS here in iOS Simulator, then do Simulator -> "Reset Content and Settings..."
// This will clear bad auth tokens stored by other apps with the same bundle ID.
isInitialized = true
}
return didFinishLaunching
}

One solution I found was to cast to the different identity profile types such as the following:
let identityManager = AWSIdentityManager.default()
if let fbIdentityProfile = identityManager.identityProfile as? FacebookIdentityProfile {
print("didFinishLaunching - Facebook login")
} else if let upIdentityProfile = identityManager.identityProfile as? UserPoolsIdentityProfile {
print("didFinishLaunching - User Pools login")
}
I can model logic in my application around this. Not sure if there is a cleaner approach using the MobileHub helper classes or AWS APIs but this works.

Related

AWS Amplify Flutter: how to create platform endpoint?

Short version of my question: I have a Flutter mobile app that uses AWS Amplify to handle user
sign-up and log-in. Now I need to add push notification capability (with AWS SNS, APN etc.)
to the app, and in particular I need to let the app itself to create the platform endpoint.
So now I have to call the CreatePlatformEndpoint function in an Amplify Flutter
mobile app, and I don't know how to do it correctly. Any idea?
Here's what I've done so far: Firstly, I configured AWS Cognito and added Amplify
to my app so that the user can sign up and log in. The relevant code looks something
like the following:
// Sign Up
var userAttributes = {
CognitoUserAttributeKey.email: email,
};
await Amplify.Auth.signUp(
username: email,
password: password,
options: CognitoSignUpOptions(
userAttributes: userAttributes,
),
);
// Log In
var res = await Amplify.Auth.signIn(
username: email,
password: password,
);
if (res.isSignedIn) {
....
} else {
....
}
This part is successful and the code works all right.
Secondly, I configured APN, AWS SNS, and the iOS module of the app, then modified
AppDelegate to something like the following:
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
registerForNotifications()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func registerForNotifications() {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
guard granted else { return }
self?.getNotificationSettings()
}
}
private func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken tokenData: Data
) {
let token = tokenData.map { String(format: "%02.2hhx", $0) }.joined()
print("token: \(token)")
}
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.badge, .sound, .alert])
}
This part is also successful. The iPhone can obtain and print the device token,
and when I manually create the endpoint in AWS Management Console and send a notification
from there, the iPhone can receive and show the notification.
Finally, I tried to let the app automatically create the endpoint in SNS when it got
the device token. I referenced this article, and managed to modify the
didRegisterForRemoteNotificationsWithDeviceToken method to the following:
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken tokenData: Data
) {
let token = tokenData.map { String(format: "%02.2hhx", $0) }.joined()
print("token: \(token)")
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: ......, identityPoolId: ".........")
let configuration = AWSServiceConfiguration(region: ......, credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
let appArn = "arn:aws:sns:........"
let platformEndpointRequest = AWSSNSCreatePlatformEndpointInput()!
platformEndpointRequest.customUserData = "........."
platformEndpointRequest.token = token
platformEndpointRequest.platformApplicationArn = appArn
let sns = AWSSNS.default()
sns.createPlatformEndpoint(platformEndpointRequest) { response, error in
....
}
}
This part is only partially successful. The endpoint is indeed created in SNS,
and I can verify this in AWS Management Console. But there are serious problems:
the user is logged out after the above code is executed
when the user tries to log back in, the app crashes with an "unexpectedly found nil" exception
Here's the crash log:
AWSMobileClient/AWSMobileClient+SignIn.swift:66: Fatal error: Unexpectedly found nil while unwrapping an Optional value
AWSMobileClient/AWSMobileClient+SignIn.swift:66: Fatal error: Unexpectedly found nil while unwrapping an Optional value
* thread #4, queue = 'NSOperationQueue 0x107d22800 (QOS: UNSPECIFIED)', stop reason = Fatal error: Unexpectedly found nil while unwrapping an Optional value
frame #0: 0x000000019b861060 libswiftCore.dylib`_swift_runtime_on_report
libswiftCore.dylib`_swift_runtime_on_report:
-> 0x19b861060 : ret
libswiftCore.dylib`_swift_reportToDebugger:
0x19b861064 : b 0x19b861060 ; _swift_runtime_on_report
libswiftCore.dylib`_swift_shouldReportFatalErrorsToDebugger:
0x19b861068 : adrp x8, 324077
0x19b86106c : ldrb w0, [x8, #0x611]
Target 0: (Runner) stopped.
I think maybe the defaultServiceConfiguration = configuration thing conflicts
with Amplify, so I try removing that part and modify the code to:
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken tokenData: Data
) {
let token = tokenData.map { String(format: "%02.2hhx", $0) }.joined()
print("token: \(token)")
let appArn = "arn:aws:sns:........"
let platformEndpointRequest = AWSSNSCreatePlatformEndpointInput()!
platformEndpointRequest.customUserData = "........."
platformEndpointRequest.token = token
platformEndpointRequest.platformApplicationArn = appArn
let sns = AWSSNS.default()
sns.createPlatformEndpoint(platformEndpointRequest) { response, error in
....
}
}
but then the code crashes at the createPlatformEndpoint call, and here's the crash log:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The service configuration is `nil`. You need to configure `awsconfiguration.json`, `Info.plist` or set `defaultServiceConfiguration` before using this method.'
*** First throw call stack:
(0x197c1f128 0x1ab932480 0x102d7fcf4 0x197857298 0x1977faf90 0x102d7fb5c 0x102c875a0 0x102c86e4c 0x102c872c0 0x19a4d8c34 0x1978562b0 0x197857298 0x197805ce4 0x197b9e170 0x197b985d0 0x197b976a8 0x1ae247570 0x19a4b5370 0x19a4ba8ec 0x102c88400 0x197876140)
libc++abi: terminating with uncaught exception of type NSException
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
frame #0: 0x00000001c38b47b0 libsystem_kernel.dylib`__pthread_kill + 8
libsystem_kernel.dylib`__pthread_kill:
-> 0x1c38b47b0 : b.lo 0x1c38b47cc ;
0x1c38b47b4 : stp x29, x30, [sp, #-0x10]!
0x1c38b47b8 : mov x29, sp
0x1c38b47bc : bl 0x1c38911fc ; cerror_nocancel
Target 0: (Runner) stopped.
So what should I do? What's the correct way to create an endpoint in an Amplify Flutter app?
OK, I'll answer my own question. I asked the Amplify-Flutter people and learned that there was currently no way to do what I want. I was advised to create a feature request ticket in their repository. I decided to turn to another approach, i.e. creating a Lambda that did the job on the backend, and it worked fine.

IOS Firebase. User UID changing after restarting the application

If i understood correct the user UID its this is a unique uid, until the user logs out. I mean he can close/open the app many times and user UID must be the same.
I have test class:
class UserFirebase {
func authorization(completionHandler: #escaping (Result<AuthDataResult?, Error>) -> Void) {
Auth.auth().signInAnonymously { authResult, error in
if error == nil {
completionHandler(.success(authResult))
return
}
completionHandler(.failure(error))
}
}
func singOut() {
try? Auth.auth().signOut()
}
func getUserUUID() -> String? {
return Auth.auth().currentUser?.uid
}
func isAuthorized() -> Bool {
return Auth.auth().currentUser != nil
}
}
when app is running i using this class like this:
let userFirebaseManager: UserFirebase = UserFirebase()
if userFirebaseManager.isAuthorized() {
// make something
} else {
userFirebaseManager.authorization(completionHandler: {[weak self] (result) in
// make something
})
}
every time I start the app, the user isAuthorized() == false. Shouldn't it persist until I press logout?
UPD:
why does my currentUser.uid change every time I restart the application?
The UID of an anonymous user is deleted when the user signs out. It cannot be regained after that, not even when you sign in on the same device.
Also see:
How constant is the Firebase Anonymous ID
Firebase Anonymous Authentication
Firebase Anonymous Auth: Restore account
How to logout and then re authenticate an anonymous firebase user
Anonymous users are signed out once the app is closed. I'm not sure why you need to sign in anonymously AND keep that UUID known. If you need to sign in anonymously just to securely access your Firebase project, it shouldn't matter if the user gets signed out. If you want to preserve the UUID, the user needs to create an account with Auth.auth().createUser(withEmail: String, password: String, completion: ((AuthDataResult?, Error?) -> Void)

How do I get an access token for Firebase Authentication?

I'm using Firebase and FirebaseUI in my project. I want to give the user an options to delete his account, but if the user has been in the app longer than 5 minutes I need to reauthenticate again.
I'm not sure how to fill these parameters in the following method:
+ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID
accessToken:(NSString *)accessToken;
With the credentials I can call
- (void)reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *) credential
completion:(nullable FIRAuthDataResultCallback) completion
I know the ProviderId from:
Auth.auth().currentUser.providerID
But how do I get the access token?
You can call this function and get the Token!
Swift Code:
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
if let refreshedToken = InstanceID.instanceID().token() {
print("InstanceID token: \(refreshedToken)")
}
}

iOS: cannot request authorization from Spotify app

I'm developping app using spotify-iOS-SDK, i have succesfully connect my app to Spotify from Safari, but when i try to connect my app from Spotify App, it doesn't request authorization in spotify app, instead it throw me back to my app after a checkmark icon show in Spotify and it caused crash to my app because the session is null.
This is my code:
var auth = SPTAuth.defaultInstance()!
auth.redirectURL = URL(string: ENV.SPOTIFY_REDIRECT_URL
auth.clientID = ENV.SPOTIFY_CLIENT_ID
auth.requestedScopes = [SPTAuthStreamingScope, SPTAuthPlaylistReadPrivateScope,
SPTAuthPlaylistModifyPublicScope, SPTAuthPlaylistModifyPrivateScope]
if SPTAuth.supportsApplicationAuthentication(){
UIApplication.shared.openURL(auth.spotifyAppAuthenticationURL())
}else{
if UIApplication.shared.openURL(auth.spotifyWebAuthenticationURL()){
if auth.canHandle(auth.redirectURL) {
// To do - build in error handling
}
}
}
I have put spotify-action in my LSApplicationQueriesSchemes. What am i doing wrong here? I saw DemoProject from https://github.com/spotify/ios-sdk
and it worked. It should request authorization right after my app go to Spotify App
You need some code in your AppDelegate that looks like follows:
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
let auth = SPTAuth()
if auth.canHandle(auth.redirectURL) {
auth.handleAuthCallback(withTriggeredAuthURL: url, callback: { (error, session) in
// Do other things to save session...
return true
}
return false
}
i have resolved my issues, the problem is my Bundle ID in my app is different from my BundleID in my Spotify Dashboard.
https://developer.spotify.com/my-applications
Just match my bundle ID and it worked!

Firebase authentication: linking multiple accounts in Swift

I've set up Firebase authentication for my iOS app using Facebook, Google & email/password sign in and it's all working fine. This authentication only happens when the user wants to access high-priority parts of my app (i.e. I don't require users to sign in to start using the app).
On app start up, I sign users in anonymously in the background and that's working fine too.
I've read the documentation but I'm struggling to understand the code required to enable me to link an anonymous account to a Facebook/email signed in account in the following flow:
new user opens app
user signed in anonymously in the background (new user.uid "A" created)
low priority data stored against anonymous user in Firebase realtime DB
user hits a high-priority area so needs to authenticate
user signs in using Facebook (new user.uid "B" created)
previous user.uid "A" needs to be linked to user.uid "B"
My method currently looks like this:
func signupWithFacebook(){
// track the anonymous user to link later
let prevUser = FIRAuth.auth()?.currentUser
FBSDKLoginManager().logInWithReadPermissions(["public_profile", "email"], fromViewController: self) { (result, error) in
if let token = result?.token?.tokenString {
let credential = FIRFacebookAuthProvider.credentialWithAccessToken(token)
FIRAuth.auth()?.signInWithCredential(credential, completion: { (user, error) in
if user != nil && error == nil {
// Success
self.success?(user: user!)
dispatch_async(dispatch_get_main_queue(), {
self.dismissViewControllerAnimated(true, completion: nil)
})
}
})
}
}
}
Any pointers to remove the confusion would be great.
UPDATE:
I've realised I was confused about the app logic because of users being created during testing. Instead of 2 separate users being created for the above scenario (one authenticated via Facebook and another anonymously), all that happens is that the original anonymous user.uid "A" is "linked" to some Facebook authentication credentials. In the Firebase console this is shown by the anonymous uid changing from anonymous to one with the Facebook logo next to it.
This is what my working method looks like:
func signupWithFacebook(){
FBSDKLoginManager().logInWithReadPermissions(["public_profile", "email"], fromViewController: self) { (result, error) in
if let token = result?.token?.tokenString {
let credential = FIRFacebookAuthProvider.credentialWithAccessToken(token)
FIRAuth.auth()?.currentUser!.linkWithCredential(credential) { (user, error) in
if user != nil && error == nil {
// Success
self.success?(user: user!)
dispatch_async(dispatch_get_main_queue(), {
self.dismissViewControllerAnimated(true, completion: nil)
})
} else {
print("linkWithCredential error:", error)
}
}
}
}
}
So your code follows the first 2 steps in this link. But the documentation explicity says not to call signInWithCredential but instead call
FIRAuth.auth()?.currentUser.linkWithCredential(credential) { (user, error) in
// ...
}
After getting your credential from Facebook's SDK.
Quote from link: "If the call to linkWithCredential:completion: succeeds, the user's new account can access the anonymous account's Firebase data."