Using AWS Amplify Auth with iOS Combine - ios

I am trying to use AWS Amplify Auth with iOS Combine to integrate social network logins (Google, Facebook and Apple) but I blocked with:
func signInWithFacebook(context: Any) -> AnyPublisher<AuthSignInResult, AuthError> {
guard let window = context as? UIWindow else {
return Fail(error: AuthError(error: Foo.unexpectedError(reason: "empty context")))
.eraseToAnyPublisher()
}
return Amplify.Auth.signInWithWebUI(for: .facebook, presentationAnchor: window)
.resultPublisher
.eraseToAnyPublisher()
}
FooError is a basic enum
enum FooError: Error {
case unexpectedError(reason: String)
}
However, I cannot seem to get the Fail construct correct, I am getting the following error: 'AuthSignInStep' cannot be constructed because it has no accessible initializers
Thoughts on how I can construct the Fail mechanism?

Related

Swift - Alamofire - Combine - try to fetch access token if the endpoint returns 401 error code

Ive got an API endpoint that i have got to take some data from but it needs an access token.
This access token is fetched from another endpoint of this api.
The access token expires every 2:30 hrs.
The way I am handling this is that every 2:20 hrs i have a a timer that fetches a new token. I know this is a bad practice since the user might turn off the internet during that fetching etc.
I am using an architectural pattern that splits my main app into 3 seperate layers.
A domain layer which contains all my models, use cases and repositories
A presentation layer which contains all my views and viewmodels.
And a Data layer that contains all my repository implementations , network constants, url builders and my API client where the request is made with alamofire.
My ApiClient is this :
public enum ApiClient {
static func requestCodable<T: Codable>(_ urlConvertible: URLRequestConvertible) -> AnyPublisher<DataResponse<T, NetworkErrorResponse>, Never> {
return AF.request(urlConvertible)
.validate()
.publishDecodable(type: T.self, emptyResponseCodes: [200])
.map { response in
response.mapError { error in
let backendError = response.data.flatMap { try? JSONDecoder().decode(BackendError.self, from: $0) }
return NetworkErrorResponse(initialError: error, backendError: backendError)
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
So , lets assume that i am making a call to an endpoint that needs the access token, and it fails because 3 hours have gone by ...
How can I say to my api client to fetch a new access token and then retry the endpoint that failed ?
Thanks for any help in advance.
Since I'm not aware of your implementation I'll try to give you what I think is a possible solution:
public enum ApiClient {
static func requestCodable<T: Codable>(_ urlConvertible: URLRequestConvertible, isRetry: Bool = false) -> AnyPublisher<DataResponse<T, NetworkErrorResponse>, Never> {
return AF
.request(urlConvertible)
.validate()
.publishDecodable(type: T.self, emptyResponseCodes: [200])
.map { response in
response.mapError { error in
let backendError = response.data.flatMap { try? JSONDecoder().decode(BackendError.self, from: $0) }
return NetworkErrorResponse(initialError: error, backendError: backendError)
}
}
.flatMap { result -> AnyPublisher<DataResponse<T, NetworkErrorResponse>, Never> in
if !isRetry && result == "401 error code" {
return requestAccessToken()
.flatMap({
// probably build a new urlConvertible with the new token
self.requestCodable(urlConvertible, isRetry: true)
})
} else {
return Just(result).eraseToAnyPublisher()
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
You could add a flatMap after your map and verify if you got the 401 error. If so, you would need to get the token, then use flatMap to re-request the requestCodable. You might notice there's a isRetry param, it is there to avoid loops.

Passing LWA token to Cognito

I am working a an app which uses the Alexa Voice Service and maintains different users, so the users needs to login with Amazon (LWA). I have implemented it like it is written in the docs and it works flawlessly.
LWA docs: https://developer.amazon.com/de/docs/login-with-amazon/use-sdk-ios.html
AMZNAuthorizationManager.shared().authorize(request, withHandler: {(result : AMZNAuthorizeResult?, userDidCancel : Bool, error : Error?) -> () in
if error != nil {
// Handle errors from the SDK or authorization server.
}
else if userDidCancel {
// Handle errors caused when user cancels login.
}
else {
// Authentication was successful.
// Obtain the access token and user profile data.
self.accessToken = result!.token
self.user = result!.user!
}
})
Furthermore I need to retrieve information from DynamoDB, which uses Cognito for authentification. As stated in the docs, there should be a way pass the access token form LWA to Cognito, but I can't find the proper place to do it. They say LWA provides an AMZNAccessTokenDelegate, which it does not. The delegate method provides an API result which Cognito needs. The link in the Cognito docs below refers to the same exact link from the LWA docs I posted above.
Cognito docs: https://docs.aws.amazon.com/cognito/latest/developerguide/amazon.html
func requestDidSucceed(apiResult: APIResult!) {
if apiResult.api == API.AuthorizeUser {
AIMobileLib.getAccessTokenForScopes(["profile"], withOverrideParams: nil, delegate: self)
} else if apiResult.api == API.GetAccessToken {
credentialsProvider.logins = [AWSCognitoLoginProviderKey.LoginWithAmazon.rawValue: apiResult.result]
}
}
What am I missing?
[EDIT]
I crawled through the LWA sources today until I finally found the correct delegate method.
Use AIAuthenticationDelegate instead of AMZNAccessTokenDelegate
But that lets me sit in front of the next two problems:
I.
Value of type 'AWSCognitoCredentialsProvider' has no member 'logins'
Maybe I have to use the following?
.setValue([AWSCognitoLoginProviderKey.LoginWithAmazon.rawValue: apiResult.result], forKey: "logins")
II.
Use of unresolved identifier 'AWSCognitoLoginProviderKey'
What do I put here? Maybe the API key I got from LWA?
[EDIT2]
I wanted to try it out, but requestDidSucceed never gets called, even through I successfully logged in.
class CustomIdentityProvider: NSObject, AWSIdentityProviderManager {
func logins() -> AWSTask<NSDictionary> {
return AWSTask(result: loginTokens)
}
var loginTokens : NSDictionary
init(tokens: [String : String]) {
self.loginTokens = tokens as NSDictionary
}
}
in the Authorization code at this below in successsful
AMZNAuthorizationManager.shared().authorize(request) { (result, userDidCancel, error) in
if ((error) != nil) {
// Handle errors from the SDK or authorization server.
} else if (userDidCancel) {
// Handle errors caused when user cancels login.
} else {
let logins = [IdentityProvider.amazon.rawValue: result!.token]
let customProviderManager = CustomIdentityProvider(tokens: logins)
guard let apiGatewayEndpoint = AWSEndpoint(url: URL(string: "APIGATEWAYURL")) else {
fatalError("Error creating API Gateway endpoint url")
}
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USWest2, identityPoolId: "IDENTITY_ID", identityProviderManager:customProviderManager)
let configuration = AWSServiceConfiguration(region: .USWest2, endpoint: apiGatewayEndpoint, credentialsProvider: credentialsProvider)
}

Moya - unable to call apis with authentication credentials

I am developing an iOS app using django rest framework for apis. But currently I am not be able to getting ahead when calling apis with authentication credentials.
I succeeded in calling the api using Postman and curl by setting Header as Authentication Bearer <token>.. but I continuously failed at calling it from iOS app. I am using Moya for calling api. And I don't know what I should do next.
What I tried: (when calling Moya)
let token = "abcde12345sometoken"
let plugin = AccessTokenPlugin(tokenClosure: token)
let provider = MoyaProvider<AccountAPI>(plugins : [plugin])
provider.request(.getAccountProfile(oauth_id: oauth_id, provider: "facebook")) { (result) in
// doing something with result
}
and configured API as:
extension AccountAPI : TargetType, AccessTokenAuthorizable {
// codes conforming variables to TargetType protocol
public var authorizationType: AuthorizationType {
switch self {
case .getFacebookAccountToken:
return .none
default:
return .bearer
}
}
public var headers: [String: String]? {
switch self {
case .getFacebookAccountToken, .getEmailAccountToken: // post requests
return ["Content-type":"application/x-www-form-urlencoded"]
default:
return ["Content-type":"application/json"]
}
}
}
Is there anything I should consider when using Moya for authentication or maybe with Info.plist and so on?
Or the document says this approach is for JWT token, and maybe my method is not for JWT and something else..? Give me some advice!
For my case, I use
Moya 12.0.1
MultiTarget
example:
plugins = [AccessTokenPlugin(tokenClosure: {
let token = ...
return token
})]
MoyaProvider<MultiTarget>(
plugins: plugins
)
.request(MultiTarget(myAPI)) {
...
}
But it never calls tokenClosure
Solution
you need to add this extension
extension MultiTarget: AccessTokenAuthorizable {
public var authorizationType: AuthorizationType {
guard let target = target as? AccessTokenAuthorizable else { return .none }
return target.authorizationType
}
}
source: https://github.com/Moya/Moya/blob/master/Sources/Moya/Plugins/AccessTokenPlugin.swift#L62
After a few hours of trying this and that.. I found out that it was the api endpoint redirects itself based on the content-language.. so the header that I set is dead when being redirected. So either setting the i18n url in advance or setting the content-language header would solve my problem.

Cognito getIdentityId() does not work in swift for unauthenticated user

I have the following code for getting the Cognito ID via unauthenticated user in swift for an IOS app:-
import Foundation
import AWSCore
import AWSCognito
class CognitoInit {
func getCognitoIdentityId() -> String {
let credentialsProvider = AWSCognitoCredentialsProvider(regionType:.USEast1,
identityPoolId:"My_idenitity_pool_id_in_aws")
let configuration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration
credentialsProvider.getIdentityId().continueWithBlock { (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
return task.error?.localizedDescription
}
else {
// the task result will contain the identity id
return task.result
}
return nil
}
return "DEFAULT_COGNITO_ID"
}
}
when getCognitoIdentityId() in a stub ViewController is called it always returns "DEFAULT_COGNITO_ID"(I am running it in an emulator). Stepping into the code via a debugger reveals that the async task does not run inside (the entire code block from if (task.error != nil) { to return nil is bypassed.
Am I missing something. Things that I can think of
Are there permissions needed for network/async calls that must be separately enabled?
Are there configurations needed in Cognito Identity Pool that I must do.
Are async tasks blocked in emulators?
Please help. I am new to swift development.
It sounds like it might just be the asynchronous nature of that call in play. Can you try the example exactly as described in the documentation, wait a few seconds, then try to get the identity id via
credentialsProvider.identityId
and see what happens? Since it is asynchronous, having it setup in this way won't work the way you're hoping, I think. You'll need to initiate the async call beforehand.

How to authenticated Google Cloud Endpoint call from an iOS client

My google backend APIs are using Oauth 2.0, when i call one of my api with that code :
func userService() -> GTLServiceUserController{
var service : GTLServiceUserController? = nil
if service == nil {
service = GTLServiceUserController()
service?.retryEnabled = true
}
return service!
}
And
func callApi() {
let service = userService()
let query = GTLQueryUserController.queryForGetAllUsers()
service.executeQuery(query) { (ticket: GTLServiceTicket!, object: AnyObject!, error: NSError!) -> Void in
if(error != nil){
print("Error :\(error.localizedDescription)")
return
}
let resp = object as? GTLUserControllerOperationResponse
if(resp != nil){
print(resp)
}
}
}
I'm getting The operation couldn’t be completed. (com.google.appengine.api.oauth.OAuthRequestException: unauthorized)
I already read this article, but i don't want my users to log in, instead i want my app to present its credentials to the service for authentication without user interaction.
I want to use this approach, but they are not telling how to do it with iOS apps.
So basically i want to be able to call my APIs successfully without any user interaction or sign-in dialog.
Please note that i have those classes on my project :
GTMOAuth2Authentication
GTMOAuth2SignIn
GTMOAuth2ViewControllerTouch
Thank's in advance
If you don't need to authenticate individual users (via OAuth2 login), then you do not need to do anything else and your App can directly call the API (as described in the "Calling the Endpoint API (unauthenticated)" section).
Service accounts are for server-to-server interactions and cannot be used to authenticate individual users; if you embed a service account & key within your iOS App, anyone can extract it from the App and start calling your API, hence it does not add any security.

Resources