I writing an app in Swift 4 that uses the Discogs API. As such, I require a user to have access to personal data on their Discogs account, so I am authenticating against their API using OAuthSwift. Currently, I am able to kick off the auth flow, sign in and return the an oauthToken and the oauthTokenSecret
Making a subsequent request to their https://api.discogs.com/oauth/identity I am returned a user object, so I am happy at this point I can sign in and make authenticated requests.
However, I do not understand how I can check if a user is authenticated when the app first starts up. Currently, I am not storing the response, instead I am making a call to the identity endpoint in nested callback
import UIKit
import OAuthSwift
class ViewController: UIViewController {
let oauthSwift = OAuth1Swift(
consumerKey: "foo",
consumerSecret: "bar",
requestTokenUrl: "https://api.discogs.com/oauth/request_token",
authorizeUrl: "https://www.discogs.com/oauth/authorize",
accessTokenUrl: "https://api.discogs.com/oauth/access_token"
)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .white
kickOffAuthFlow()
}
fileprivate func kickOffAuthFlow() {
oauthSwift.authorizeURLHandler = SafariURLHandler(viewController: self, oauthSwift: oauthSwift)
guard let callbackURL = URL(string: "foo.bar.boobaz:/oauth_callback") else { return }
oauthSwift.authorize(withCallbackURL: callbackURL, success: { (credential, response, parameters) in
_ = self.oauthSwift.client.get("https://api.discogs.com/oauth/identity", success: { (response) in
guard let dataString = response.string else { return }
print(dataString)
}, failure: { (error) in
print("error")
})
}) { (error) in
print(error.localizedDescription)
}
}
}
What is best practice in this case? How should I store these tokens and how should I ensure once the user is logged in, they aren't forced to log in the next time the app is opened (providing the token hasn't expired, however that is a separate issue I am prepared to handle at a later point)
Coming from a web development background, I was able to just store a token in session storage, on load I would then check the exp on the token and request a new one or take some other action.
I have not quite grasped how this works in iOS development yet.
You have two options to store access token in local.
UserDefault
Keychain
1. UserDefault
Use UserDefault to store token in memory. When the app gets launch, check if the token is stored in userdafault. UserDefault is used as short memory storage where you can store small data. It remains in memory if you kill the app.
let tokenIdentifier = "TokenIdentifier"
func storeAccessToken(token: String) {
UserDefaults.standard.set(token, forKey: tokenIdentifier)
}
func checkUserLogin() {
if UserDefaults.standard.value(forKey: tokenIdentifier) != nil {
print("User is Login")
}
else {
print("User need to login")
}
}
check this for learn more about userdefault
https://swift3tutorials.com/swift-3-user-defaults/
https://www.hackingwithswift.com/example-code/system/how-to-save-user-settings-using-userdefaults
2. Keychain
Userdefault is not secure. An access token is a sensitive information which should be stored in a secure place. So storing the access token in user default is not the correct choice. You must store access token in the keychain. Use SwiftKeychainWrapper pod to store token in Keychain.
let tokenIdentifier = "TokenIdentifier"
func storeAccessToken(token: String) {
KeychainWrapper.standard.set(token, forKey: tokenIdentifier)
}
func checkUserLogin() {
let token: String? = KeychainWrapper.standard.string(forKey: tokenIdentifier)
if token != nil {
print("User is Login")
}
else {
print("User need to login")
}
}
Related
I want user to login once and not have to reenter their login info everytime they open app unless they logout in the last session.
Login screen is currently displayed everytime the app is open. This is my rootview
struct AppRootView: View {
var body: some View {
AnyView {
// check if user has already logged in here and then route them accordingly
if auth.token != nil {
homeMainView()
} else {
LoginController()
}
}
}
}
currently this is what I use to login users
#objc func signUp() {
setLoading(true);
app.usernamePasswordProviderClient().registerEmail(username!, password: password!, completion: {[weak self](error) in
// Completion handlers are not necessarily called on the UI thread.
// This call to DispatchQueue.main.sync ensures that any changes to the UI,
// namely disabling the loading indicator and navigating to the next page,
// are handled on the UI thread:
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
print("Signup failed: \(error!)")
self!.errorLabel.text = "Signup failed: \(error!.localizedDescription)"
return
}
print("Signup successful!")
// Registering just registers. Now we need to sign in, but we can reuse the existing username and password.
self!.errorLabel.text = "Signup successful! Signing in..."
self!.signIn()
}
})
}
#objc func signIn() {
print("Log in as user: \(username!)");
setLoading(true);
app.login(withCredential: AppCredentials(username: username!, password: password!)) { [weak self](maybeUser, error) in
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
// Auth error: user already exists? Try logging in as that user.
print("Login failed: \(error!)");
self!.errorLabel.text = "Login failed: \(error!.localizedDescription)"
return
}
guard let user = maybeUser else {
fatalError("Invalid user object?")
}
print("Login succeeded!");
//
let hostingController = UIHostingController(rootView: ContentView())
self?.navigationController?.pushViewController(hostingController, animated: true)
}
how could I implement one time login so that users do have to login each time they open the app?
A correctly configured and initialized RealmApp class will persist the session information for you between app restarts, you can check for an existing session using the .currentUser() method from this class. So in your case something like:
if app.currentUser() != nil {
homeMainView()
} else {
LoginController()
}
While using Realm to persist login is a good idea, but I would highly
advice against using it for managing user authentication credentials such
as passwords. A better approach if you want to save sensitive information is
using KeyChain just like what Apple and password manager apps do. With a light
weight keyChain wrapper library such as SwiftKeychainWrapper You can easily
save your login credentials in the most secure way.
Here is a sample using a keyChain wrapper linked above.
With simple modification you can use this helper class to manage your sign in credentials anywhere in your app.
import SwiftKeychainWrapper
class KeyChainService {
// Make a singleton
static let shared = KeyChainService()
// Strings which will be used to map data in keychain
private let passwordKey = "passwordKey"
private let emailKey = "emailKey"
private let signInTokenKey = "signInTokenKey"
// Saving sign in info to keyChain
func saveUserSignInInformation(
email: String,
password: String,
token: String
onError: #escaping() -> Void,
onSuccess: #escaping() -> Void
) {
DispatchQueue.global(qos: .default).async {
let passwordIsSaved: Bool = KeychainWrapper.standard.set(password, forKey: self.passwordKey)
let emailIsSaved: Bool = KeychainWrapper.standard.set(email, forKey: self.emailKey)
let tokenIsSaved: Bool = KeychainWrapper.standard.set(token, forKey: self.signInTokenKey)
DispatchQueue.main.async {
// Verify that everything is saved as expected.
if passwordIsSaved && emailIsSaved && tokenIsSaved {
onSuccess()
}else {
onError()
}
}
}
}
// Retrieve signIn information for auto login
func retrieveSignInInfo(onError: #escaping() -> Void, onSuccess: #escaping(UserModel) -> Void) {
DispatchQueue.main.async {
let retrievedPassword: String? = KeychainWrapper.standard.string(forKey: self.passwordKey)
let retrievedEmail: String? = KeychainWrapper.standard.string(forKey: self.emailKey)
let retrievedToken: String? = KeychainWrapper.standard.string(forKey: self.signInTokenKey)
if let password = retrievedPassword,
let email = retrievedEmail,
let token = retrievedToken {
// Assuming that you have a custom user model named "UserModel"
let user = UserModel(email: email, password: password,token: token)
// Here is your user info which you can use to verify with server if needed and auto login user.
onSuccess(user)
}else {
onError()
}
}
}
}
I am currently working on the signup/login of my app using Firebase Authentication.
I have custom buttons for Facebook Signup/Login and Google Signup/Login.
For example, the code for the Facebook Signup/Login button is
#objc private func performFacebookLogin(_ sender: UIButton) {
let fbLoginManager : LoginManager = LoginManager()
fbLoginManager.logIn(permissions: ["email"], from: self) { (result, error) -> Void in
if let error = error {
print(error)
} else {
if result == nil {
print("no result!")
return
}
if AccessToken.current == nil {
print("no token")
return
}
DispatchQueue.main.async {
ThirdPartySignupViewController.push(fromVC: self, method: .facebook)
}
}
}
}
This will take the user to another view controller to add additional information to their account if they have not yet created an account in the app using this Facebook account.
However, I can not find any way to determine whether or not the Facebook account has previously used for an account in the app, in which case I want to just log the user in and take them to the main screen. Is there a way to do this?
I have tried the signIn(with:) method, hoping that it would return an error if there is no account in the app with the credentials of this Facebook account, but this will not work because this will end up creating a new account if the Facebook account hasn't been used previously to create an account.
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (authResult, error) in
//check if I get an error if the Facebook account has not been used
}
(EDIT) The desired flow is 1.Sign in with Facebook/Google → 2.Enter additional info → 3.Create account on Firebase Authentication.
When authenticating with Firebase - the Auth object returns isNewUser through additionalUserInfo. Therefore you can see the status of a new or existing user.
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (authResult, error) in
//authResult?.additionalUserInfo?.isNewUser
if let auth = authResult{
if auth.additionalUserInfo?.isNewUser {
// User has not been authenticated before. Do what you need
} else {
// User has been authenticated before
}
}
}
i am integrating Pinterest login in my swift app but the issue is that i am login success fully but when i click on button logout i am not able to logout here is my code
Code
#IBAction func btnLogin(_ sender: UIButton) {
PDKClient.sharedInstance().authenticate(withPermissions: [PDKClientReadPublicPermissions,PDKClientWritePublicPermissions,PDKClientReadRelationshipsPermissions,PDKClientWriteRelationshipsPermissions], withSuccess: { (PDKResponseObject) in
self.accessToken = PDKClient.sharedInstance().oauthToken
//MARK: - Getting User Profile, use "/v1/me" to get user data in the Response object -
let parameters : [String:String] =
[
"fields": "first_name,id,last_name,url,image,username,bio,counts,created_at,account_type" //these fields will be fetched for the loggd in user
]
PDKClient.sharedInstance().getPath("/v1/me/", parameters: parameters, withSuccess: {
(PDKResponseObject) in
self.view.isUserInteractionEnabled = true
self.user = (PDKResponseObject?.user())!
print(PDKResponseObject!)
self.lblUserName.text = PDKResponseObject?.user().username
if let url = JSON(PDKResponseObject?.user().images["60x60"] as Any)["url"].string
{
self.imgProPic.sd_setImage(with: URL(string: url))
}
}) {
(Error) in
if let error = Error
{
print("\(Error)")
}
self.view.isUserInteractionEnabled = true
PDKPinError.unknown
}
}) {
(Error) in
self.view.isUserInteractionEnabled = true
}
}
#IBAction func btnLogout(_ sender: UIButton) {
/*
let cookieJar : HTTPCookieStorage = HTTPCookieStorage.shared
for cookie in cookieJar.cookies! as [HTTPCookie]{
NSLog("cookie.domain = %#", cookie.domain)
if cookie.domain == "www.pinterest.com" ||
cookie.domain == "api.pinterest.com"{
cookieJar.deleteCookie(cookie)
}
}
*/
PDKClient.clearAuthorizedUser()
}
here i have done clearAuthorizedUser() but not logged out and also i have done cookie clear code but thats not working please help me to solve this
I had a very similar issue, and I thought I might be able to help someone by posting my solution here. The app that I'm maintaining authenticates Pinterest by loading "https://api.pinterest.com/oauth/" in a UIWebView with the appropriate parameters. This authentication approach does not require the Pinterest SDK and is explained in detail here: https://developers.pinterest.com/docs/api/overview/. I was able to add a button that the user can tap to log them out of their authenticated Pinterest account. To log the user out, I removed all cookies associated with Pinterest Authentication, and removed any stored authentication token. Below is the code I use to remove the cookies:
let cookieJar: HTTPCookieStorage = HTTPCookieStorage.shared
if let cookies: [HTTPCookie] = cookieJar.cookies {
for cookie in cookies {
let domain = cookie.domain
if domain == "accounts-oauth.pinterest.com"
|| domain == ".pinterest.com"
|| domain == ".facebook.com"
|| domain == "accounts.google.com" {
cookieJar.deleteCookie(cookie)
}
}
}
After that, you will most likely need to call PDKClient.clearAuthorizedUser() to remove any stored authentication token.
I'm using https://github.com/auth0/socketio-jwt to connect the user to my node.js/socket.io server and I'm using one round trip
My problem right now is that whenever user logs in on the IOS part, the socket.connect() is not consistent, my theory is that the token is not yet ready even before the socket.connect() gets invoked.
I'm using Singleton design for my Socket.io class as many people pointed that out.
Here's the code on the SocketManager.swift part
import SocketIO
class SocketIOManager: NSObject {
static let sharedInstance = SocketIOManager()
var socket = SocketIOClient(socketURL: URL(string: mainURL)!, config: [.log(false), .compress, .connectParams(["token": getToken()])]) // getToken() I got it from other file which is Constant.Swift
func establishConnection() {
socket.connect()
}
func closeConnection() {
socket.disconnect()
}
}
I'm using KeychainAccess to store the token and Constant.Swift file store all the global variables and functions so that I could call it on any Swift files.
Constant.Swift
import Foundation
import KeychainAccess
let keychain = Keychain(server: "www.example.com", protocolType: .https)
func getToken() -> String {
if let token = keychain["token"] {
return token
}
return ""
}
LoginViewController.swift
#IBAction func facebookButtonClicked(_ sender: UIButton) {
Alamofire.request("/login", method: .post, parameters: parameters, encoding: JSONEncoding.default)
.responseJSON { response in
if let value = response.result.value {
let json = JSON(value)
self.keychain["token"] = String(describing: json["token"])
SocketIOManager.sharedInstance.establishConnection()
self.segueToAnotherVC() // Segue to another screen, to simplify things i put it in a function
}
}
}
So technically what is happening in this controller is, when the user logs in, I will store the token into KeychainAccess (it is equivalent to NSUserDefaults), then only I will make a socket connection because the socket connection needs a token beforehand.
What should I do to make the connection consistent all the time, whenever user logs in? Any methods that I could use?
I suggest you to use keychain like this:
let keychain = KeychainSwift()
keychain.set("string", forKey: "key")
keychain.get("key")
keychain.delete("key")
keychain Usage:
let saveBool: Bool = KeychainWrapper.setString("String", forKey: "key")
let retrievedString: String? = KeychainWrapper.stringForKey("key")
let removeBool: Bool = KeychainWrapper.removeObjectForKey("key")
And make sure that your token is set when calling establish connection, if not, don't try and connect.
References:
https://github.com/socketio/socket.io-client-swift/issues/788
https://github.com/marketplacer/keychain-swift
https://github.com/jrendel/SwiftKeychainWrapper
More info:
JSON Web Token is a JSON-based open standard for creating access tokens that assert some number of claims.
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.