We retrieve any saved passwords through the function:
SecRequestSharedWebCredential(NULL, NULL, ^(CFArrayRef credentials, CFErrorRef error) {
if (!error && CFArrayGetCount(credentials)) {
CFDictionaryRef credential = CFArrayGetValueAtIndex(credentials, 0);
if (credential > 0) {
CFDictionaryRef credential = CFArrayGetValueAtIndex(credentials, 0);
NSString *username = CFDictionaryGetValue(credential, kSecAttrAccount);
NSString *password = CFDictionaryGetValue(credential, kSecSharedPassword);
dispatch_async(dispatch_get_main_queue(), ^{
//Updates the UI here.
});
}
}
});
The issue is that on IOS 9.3.3 iPhone 6 A1524, we get the prompt with an entry called 'Passwords not saved'. There is no error message to suggest the no passwords have been found. Because the array > 0, it completes the form with the entry.
Why is this the case? We thought the prompt does not appear if no passwords are stored under your entitled domains.
Any suggestions?
Thank you.
I'm checking for this in viewDidLoad() for my Auth view controller. The code is a bit different than above, gleaned from several other SO answers.
Swift 3:
SecRequestSharedWebCredential(Configuration.webBaseFQDN as CFString, nil, { (credentials, error) in
if let error = error {
print("ERROR: credentials")
print(error)
}
guard let credentials = credentials, CFArrayGetCount(credentials) > 0 else {
// Did not find a shared web credential.
return
}
guard CFArrayGetCount(credentials) == 1 else {
// There should be exactly one credential.
return
}
let unsafeCredential = CFArrayGetValueAtIndex(credentials, 0)
let credential = unsafeBitCast(unsafeCredential, to: CFDictionary.self)
let unsafeEmail = CFDictionaryGetValue(credential, Unmanaged.passUnretained(kSecAttrAccount).toOpaque())
let email = unsafeBitCast(unsafeEmail, to: CFString.self) as String
let unsafePassword = CFDictionaryGetValue(credential, Unmanaged.passUnretained(kSecSharedPassword).toOpaque())
let password = unsafeBitCast(unsafePassword, to: CFString.self) as String
if self.isValidEmail(email) && self.isValidPassword(password) {
self.usedSharedWebCredentials = true
self.doSignIn(email: email, password: password)
}
})
The extra check at the end for isValidEmail(_:) and isValidPassword(_:) handles the case where SecRequeestSharedWebCredential() returns "Passwords not saved" in the first credential (email).
Hopefully someone can explain why this is happening, but if not, at least there's a way to trap this scenario.
I'd also like to add that I've seen this up to iOS 10.2.1
I bumped into the same issue and wanted to add that the spaces in "Passwords not saved" are not real spaces. Not sure why, maybe something odd when converting from CFString.
Either way, since Apple documentation is still in ObjC and their Security framework is still very much CoreFoundation-heavy, I thought it'd be nice to post the whole Swift 5 code I've written for the Shared Web Credentials wrapper.
It has nice error management logic (to adjust since you might not have the same ErrorBuilder API). About the weird spaces, when copied from Xcode to StackOverflow, they turn into real spaces, hence the extra logic in the String extension.
There is nothing better online from what I've seen.
//
// CredentialsRepository.swift
// Created by Alberto De Bortoli on 26/07/2019.
//
import Foundation
public typealias Username = String
public typealias Password = String
public struct Credentials {
public let username: Username
public let password: Password
}
public enum GetCredentialsResult {
case success(Credentials)
case cancelled
case failure(Error)
}
public enum SaveCredentialsResult {
case success
case failure(Error)
}
protocol CredentialsRepository {
func getCredentials(completion: #escaping (GetCredentialsResult) -> Void)
func saveCredentials(_ credentials: Credentials, completion: #escaping (SaveCredentialsResult) -> Void)
}
//
// SharedWebCredentialsController.swift
// Created by Alberto De Bortoli on 26/07/2019.
//
class SharedWebCredentialsController {
let domain: String
init(domain: String) {
self.domain = domain
}
}
extension SharedWebCredentialsController: CredentialsRepository {
func getCredentials(completion: #escaping (GetCredentialsResult) -> Void) {
SecRequestSharedWebCredential(domain as CFString, .none) { cfArrayCredentials, cfError in
switch (cfArrayCredentials, cfError) {
case (_, .some(let cfError)):
let underlyingError = NSError(domain: CFErrorGetDomain(cfError) as String,
code: CFErrorGetCode(cfError),
userInfo: (CFErrorCopyUserInfo(cfError) as? Dictionary))
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsFetchFailure, underlyingError: underlyingError)
DispatchQueue.main.async {
completion(.failure(error))
}
case (.some(let cfArrayCredentials), _):
if let credentials = cfArrayCredentials as? [[String: String]], credentials.count > 0,
let entry = credentials.first,
// let domain = entry[kSecAttrServer as String]
let username = entry[kSecAttrAccount as String],
let password = entry[kSecSharedPassword as String] {
DispatchQueue.main.async {
if username.isValidUsername() {
completion(.success(Credentials(username: username, password: password)))
}
else {
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsFetchFailure, underlyingError: nil)
completion(.failure(error))
}
}
}
else {
DispatchQueue.main.async {
completion(.cancelled)
}
}
case (.none, .none):
DispatchQueue.main.async {
completion(.cancelled)
}
}
}
}
func saveCredentials(_ credentials: Credentials, completion: #escaping (SaveCredentialsResult) -> Void) {
SecAddSharedWebCredential(domain as CFString, credentials.username as CFString, credentials.password as CFString) { cfError in
switch cfError {
case .some(let cfError):
let underlyingError = NSError(domain: CFErrorGetDomain(cfError) as String,
code: CFErrorGetCode(cfError),
userInfo: (CFErrorCopyUserInfo(cfError) as? Dictionary))
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsSaveFailure, underlyingError: underlyingError)
DispatchQueue.main.async {
completion(.failure(error))
}
case .none:
DispatchQueue.main.async {
completion(.success)
}
}
}
}
}
extension String {
fileprivate func isValidUsername() -> Bool {
// https://stackoverflow.com/questions/38698565/secrequestsharedwebcredential-credentials-contains-passwords-not-saved
// don't touch the 'Passwords not saved', the spaces are not what they seem (value copied from debugger)
guard self != "Passwords not saved" else { return false }
let containsAllInvalidWords = contains("Passwords") && contains("not") && contains("saved")
return !containsAllInvalidWords
}
}
Related
I am trying to login with macbook using code but I keep on getting this error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
// API to log an user in
func login(userType: String, completionHandler: #escaping (NSError?) -> Void) {
let path = "api/social/convert-token/"
let url = baseURL!.appendingPathComponent(path)
let params: [String: Any] = [
"grant_type": "convert_token",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"backend": "facebook",
"token": FBSDKAccessToken.current().tokenString,
"user_type": userType
]
Alamofire.request(url!, method: .post, parameters: params, encoding: URLEncoding(), headers: nil).responseJSON { (response) in
switch response.result {
case .success(let value):
let jsonData = JSON(value)
self.accessToken = jsonData["access_token"].string!
self.refreshToken = jsonData["refresh_token"].string!
self.expired = Date().addingTimeInterval(TimeInterval(jsonData["expire_in"].int!))
completionHandler(nil)
break
case .failure(let error):
completionHandler(error as NSError?)
break
}
}
}
The error is referring to this line:
self.accessToken = jsonData["access_token"].string!
and this is LoginViewController code:
import UIKit
import FBSDKLoginKit
class LoginViewController: UIViewController {
#IBOutlet weak var bLogin: UIButton!
#IBOutlet weak var bLogout: UIButton!
var fbLoginSuccess = false
var userType: String = USERTYPE_CUSTOMER
override func viewDidLoad() {
super.viewDidLoad()
if (FBSDKAccessToken.current() != nil) {
bLogout.isHidden = false
FBManager.getFBUserData(completionHandler: {
self.bLogin.setTitle("Continue as \(User.currentUser.email!)", for: .normal)
// self.bLogin.sizeToFit()
})
}
}
override func viewDidAppear(_ animated: Bool) {
if (FBSDKAccessToken.current() != nil && fbLoginSuccess == true) {
performSegue(withIdentifier: "CustomerView", sender: self)
}
}
#IBAction func facebookLogout(_ sender: AnyObject) {
APIManager.shared.logout { (error) in
if error == nil {
FBManager.shared.logOut()
User.currentUser.resetInfo()
self.bLogout.isHidden = true
self.bLogin.setTitle("Login with Facebook", for: .normal)
}
}
}
#IBAction func facebookLogin(_ sender: AnyObject) {
if (FBSDKAccessToken.current() != nil) {
APIManager.shared.login(userType: userType, completionHandler: { (error) in
if error == nil {
self.fbLoginSuccess = true
self.viewDidAppear(true)
}
})
} else {
FBManager.shared.logIn(
withReadPermissions: ["public_profile", "email"],
from: self,
handler: { (result, error) in
if (error == nil) {
FBManager.getFBUserData(completionHandler: {
APIManager.shared.login(userType: self.userType, completionHandler: { (error) in
if error == nil {
self.fbLoginSuccess = true
self.viewDidAppear(true)
}
})
})
}
})
}
}
}
Swift 3
Xcode 9
iOS 10.2
I read several texts to find out the causes of this type of error but not succeed.
First of all, it is a bad practice to use force unwrap (!) everywhere. Try to avoid it, until you really need it and/or know what are you doing. Here you use SwiftyJSON. It helps you to extract data from JSON in a convenient way. But you shouldn't rely, that you will always get proper JSON from the backend. There are many reasons why it can return wrong JSON and there would not be needed value. There are two options:
You can use .stringValue instead of .string - it will return an empty string instead of nil
You can do in this way: if let token = jsonData["access_token"].string {...} or even use guard statement
Here is a good article to understand force unwrapping: https://blog.timac.org/2017/0628-swift-banning-force-unwrapping-optionals/
This error happens when you have a ! (force unwrap symbol), meaning that you're certain the data will be there; but in fact, the data isn't there - it's nil.
Try using a guard statement. For example:
guard let self.accessToken = jsonData["access_token"].string else {
// log error message, if desired; then exit the function
return
}
Alternatively, you could use an if let statement:
if let self.accessToken = jsonData["access_token"].string {
// do stuff if 'jsonData["access_token"].string' is valid
}
else {
// do other stuff if 'jsonData["access_token"].string' is nil
}
Now, why the data is not there (nil) - that's another question. Perhaps check your JSON function to ensure it's properly processing the JSON response. Also, check to make sure you're getting a valid response:
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode == 200 else {
// handle a bad response code
return
}
// handle the good response code stuff after the guard statement
Learn about using if let to handle possible nil values in the Swift Programming Language (Swift 4.2) Guide under Optional Binding in the Basics section.
Learn about guard statements under Early Exit in the Control Flow section of the Guide, and also in the Statements section of the Reference.
I am using AWS AppSync for creating my iOS application. I want to leverage the offline mutation as well as query caching provided by AppSync. But when I am turning my internet off, I am not getting any response. Rather its showing an error as "The Internet connection appears to be offline.". This seems to be rather an Alamofire exception than an AppSync exception. This is because the query is not getting cached inside my device. Following is my code snippet to initialize the client.
do {
let appSyncClientConfig = try AWSAppSyncClientConfiguration.init(url: AWSConstants.APP_SYNC_ENDPOINT, serviceRegion: AWSConstants.AWS_REGION, userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider())
AppSyncHelper.shared.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncClientConfig)
AppSyncHelper.shared.appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error in initializing the AppSync Client")
print("Error: \(error)")
UserDefaults.standard.set(nil, forKey: DeviceConstants.ID_TOKEN)
}
I am caching the token in the UserDefaults at the time of fetching the session, and then whenever the AppSyncClient is called, it fetches the latest token by calling the getLatestAuthToken() method of my MyCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider. This is returning the token stored in the UserDefaults -
// background thread - asynchronous
func getLatestAuthToken() -> String {
print("Inside getLatestAuthToken")
var token: String? = nil
if let tokenString = UserDefaults.standard.string(forKey: DeviceConstants.ID_TOKEN) {
token = tokenString
return token!
}
return token!
}
My query pattern is the following
public func getUserProfile(userId: String, success: #escaping (ProfileModel) -> Void, failure: #escaping (NSError) -> Void) {
let getQuery = GetUserProfileQuery(id: userId)
print("getQuery.id: \(getQuery.id)")
if appSyncClient != nil {
print("AppSyncClient is not nil")
appSyncClient?.fetch(query: getQuery, cachePolicy: CachePolicy.returnCacheDataElseFetch, queue: DispatchQueue.global(qos: .background), resultHandler: { (result, error) in
if error != nil {
failure(error! as NSError)
} else {
var profileModel = ProfileModel()
print("result: \(result)")
if let data = result?.data {
print("data: \(data)")
if let userProfile = data.snapshot["getUserProfile"] as? [String: Any?] {
profileModel = ProfileModel(id: UserDefaults.standard.string(forKey: DeviceConstants.USER_ID), username: userProfile["username"] as? String, mobileNumber: userProfile["mobileNumber"] as? String, name: userProfile["name"] as? String, gender: (userProfile["gender"] as? Gender).map { $0.rawValue }, dob: userProfile["dob"] as? String, profilePicUrl: userProfile["profilePicUrl"] as? String)
} else {
print("data snapshot is nil")
}
}
success(profileModel)
}
})
} else {
APPUtilites.displayErrorSnackbar(message: "Error in the user session. Please login again")
}
}
I have used all the 4 CachePolicy objects provided by AppSync, i.e,
CachePolicy.returnCacheDataElseFetch
CachePolicy.fetchIgnoringCacheData
CachePolicy.returnCacheDataDontFetch
CachePolicy.returnCacheDataAndFetch.
Can someone help me in implementing the cache properly for my iOS app so that I can do queries without the internet also?
Okay so I found the answer myself. The databaseUrl is an optional argument. It does not come in the suggestions when we are initializing the AWSAppSyncClientConfiguration object.
So the new way in which I initialized the client is the following
let databaseURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(AWSConstants.DATABASE_NAME, isDirectory: false)
do {
let appSyncClientConfig = try AWSAppSyncClientConfiguration.init(url: AWSConstants.APP_SYNC_ENDPOINT,
serviceRegion: AWSConstants.AWS_REGION,
userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider(),
urlSessionConfiguration: URLSessionConfiguration.default,
databaseURL: databaseURL)
AppSyncHelper.shared.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncClientConfig)
AppSyncHelper.shared.appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error in initializing the AppSync Client")
print("Error: \(error)")
}
Hope it helps.
I'm trying to sign up users with Firebase auth. When a user signs up, I'd like them to be added to my Users collection in Firestore as well as the Users authorization section.
The createUser(withEmail: ...) method works every time. However, my db.collection("users").document(user.id).setData([..] method will only be called if I press the sign up button twice, and at that point the createUser(withEmail ...) method gets called again. Here's the relevant code
SignupViewController.swift
#IBAction func signupButtonTapped(_ sender: UIButton) {
// user: User() defined here
usersHelper.signup(user: user, password: password) { result in
// This closure is only executed on the second press
guard let user = result as? Firebase.User else {
let error = result as? Error
self.handleSignupError(error!)
return
}
self.performSegue(withIdentifier: "ShowGroupsFromSignupSegue", sender: self)
}
}
UsersHelper.Swift
func signup(user: User, password: String, completion: #escaping (_ result: Any?) -> Void) {
let userDispatchGroup = DispatchGroup()
var signupError: Error? = nil
var dbError: Error? = nil
var firebaseUser: Firebase.User? = nil
userDispatchGroup.enter()
usersDataModel.signupUser(user: user, password: password) { result in
// Completion handler
if result as? Error != nil {
signupError = result as? Error
} else {
// Got the user
firebaseUser = result as? Firebase.User
}
userDispatchGroup.leave()
}
userDispatchGroup.enter()
usersDataModel.create(user: user) { err in
// This will only execute if signUp is called twice
if let result = err as? Error {
print("Error msg: \(result.localizedDescription)")
dbError = result
}
print("!Created db user")
userDispatchGroup.leave()
}
userDispatchGroup.notify(queue: .main) {
print("!dispatch group completed successfully")
if (signupError == nil && dbError == nil) {
completion(firebaseUser)
} else {
signupError != nil ? completion(signupError) : completion(dbError)
}
}
}
UsersDataModel.swift
func signupUser(user: User, password: String, _ completion: #escaping (_ err: Any? ) -> Void) {
// Create user in Auth & create DB entry
Auth.auth().createUser(withEmail: user.email, password: password) { (authResult, err) in
if let err = err {
print("Error creating user \(err)")
completion(err)
} else {
print("User signed up successfully")
completion(authResult) // completion called with User
}
}
}
func create(user: User, _ completion: #escaping (_ result: Any?) -> Void) {
// userData dictionary created here
db.collection("users").document(user.ID).setData(userData) { err in
if let err = err {
print("There was an error creating the user \(err)")
completion(err)
} else {
print("!User created in db successfully!")
completion(nil)
}
}
}
Any help is greatly appreciated! Thank you all in advance
I've resolved the error. I ended up nesting the second network call in order to:
Get the uid from the firestore who was authenticated
Not break firestore rules about writing to the database w/o an authorized uid
My UsersHelper.swift file now looks like
func signup(user: User, password: String, completion: #escaping (_ result: Any?) -> Void) {
let userDispatchGroup = DispatchGroup()
var signupError: Error? = nil
var dbError: Error? = nil
var firebaseUser: Firebase.User? = nil
userDispatchGroup.enter()
usersDataModel.signupUser(user: user, password: password) { result in
// Completion handler
if result as? Error != nil {
// there was an error?
print("Error: \(result)")
signupError = result as? Error
} else {
// Got the user
firebaseUser = result as? Firebase.User
// Create user entry in DB
user.ID = firebaseUser!.uid
self.usersDataModel.create(user: user) { err in
// Completion handler
if let err = err as? Error {
dbError = err
}
userDispatchGroup.leave()
print("Done")
}
}
}
userDispatchGroup.notify(queue: .main) {
print("!dispatch group completed successfully")
if (signupError == nil && dbError == nil) {
completion(firebaseUser)
} else {
signupError != nil ? completion(signupError) : completion(dbError)
}
}
}
Solution for iOS
When submitting a forgotten password request to the aws cognito identity pool, the request must be signed with the client's secret in combination with the username being submitted in the forgot password request.
How can we create the "secretHash" from the client secret and the username within swift in the format required by aws?
This functionality is not documented and is only found in the tests of some AWS libraries. This code serves as an example of submitting a forgot password request until the functionality is better supported within the AWSCongitoIdentityUserPool library.
Swift 3.2
func forgotPassword(username: String) {
let pool = AWSCognitoIdentityUserPool.default()
let request = AWSCognitoIdentityProviderForgotPasswordRequest()
request?.username = username
request?.clientId = pool.userPoolConfiguration.clientId
request?.secretHash = pool.calculateSecretHash(username: username)
AWSCognitoIdentityProvider.default().forgotPassword(request!) { (response, error) in
if let error = error {
print(error)
}
else {
print("success")
}
}
}
Signing the username with the client secret from the user pool.
extension AWSCognitoIdentityUserPool {
func calculateSecretHash(username: String) -> String? {
guard let clientSecret = userPoolConfiguration.clientSecret else {
return nil
}
guard let key = clientSecret.data(using: String.Encoding.ascii) else {
return nil
}
guard let data = (username + userPoolConfiguration.clientId).data(using: String.Encoding.utf8) else {
return nil
}
let hmac = sign256(data: data, key: key)
return hmac.base64EncodedString()
}
fileprivate func sign256(data: Data, key: Data) -> Data {
let algorithm: CCHmacAlgorithm = CCHmacAlgorithm(kCCHmacAlgSHA256)
let digestLength = Int(CC_SHA256_DIGEST_LENGTH)
let signature = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: digestLength)
defer { signature.deallocate(capacity: digestLength) }
data.withUnsafeBytes { dataBytes in
key.withUnsafeBytes { keyBytes in
CCHmac(algorithm, keyBytes, key.count, dataBytes, data.count, signature)
}
}
return Data(bytes: signature, count: digestLength)
}
}
I'm writing some code for a login page where we take a username and find the associated password. Temporarily I've said "if email exists under username, complete segue". However when I call the method getEmail which checks for email, it never seems to exit properly with a full email address. print(email) returns the right email address so I know I've retrieved it and it's correct. I never seem to make it out of the method though. Really stuck here! Heres my code:
func getEmail(name: String) -> String{
var email = ""
ref = Database.database().reference()
self.ref.child("Users").child(name).observeSingleEvent(of: .value, with: { (snapshot) in
if let user = snapshot.value as? [String:Any] {
print("email retrieved");
email = user["email"] as! String;
print(email)
return;
}
else{
print("email could not be retrieved from the user.");
}
}){ (error) in
print("Could not retrieve object from database because: ");
print((Any).self);
}
return email;
}
func validate(){
if(Username.text == ""){
EmptyStringAlert();
}
let email = getEmail(name: Username.text!);
print(email)
if(email == ""){
return;
}
performSegue(withIdentifier: "LoginSuccess", sender: nil)
}
The call to Firebase is asynchronous, so you have to use completion in your function to get the data. Try something like this:
func getEmail(name: String, completion: #escaping (Bool, Any?, Error?) -> Void) {
var email = ""
ref = Database.database().reference()
self.ref.child("Users").child(name).observeSingleEvent(of: .value, with: { (snapshot) in
if let user = snapshot.value as? [String:Any] {
email = user["email"] as! String
completion(true, email, nil)
}
else {
completion(false, nil, nil)
}
}){ (error) in
completion(false, nil, error)
}
}
func validate(){
if(Username.text == ""){
EmptyStringAlert();
}
getEmail(name: Username.text!) { (success, response, error) in
guard success, let email = response as? String else {
print(error ?? "Failed getEmail..")
return
}
if(email == "") {
return
}
performSegue(withIdentifier: "LoginSuccess", sender: nil)
}
}