I'm developing a simple web API with Vapor. To give more context, I'm newbie in backend development.
The consumer of the API is going to be an iOS app. Currently, I don't need the users to sign up to use the app. And I would like to keep it like that.
On the other hand, I would like to have some authentication to avoid that anyone could use the API I'm developing.
Looking for information I've found how implement authentication. But the examples I've seen are based on creating users in the backend for each user of the app. What I don't want to do. I would like to use an api-key as we do normally when we use third-party api's.
How could I have "api-key authentication" with Vapor ??
Or, should I just create an unique user/password that it's shared by all the users of the iOS app (that use the API) and then use basic or token authentication?
Thank you very much!
Carlos
One way around this is to create a fake token and use either the TokenAuthenticationMiddleware or more likely a custom middleware that checks the incoming token.
However, be aware that there is nothing stopping anyone from inspecting the traffic coming from your app to view the token and then using that to access your API.
Following Tim idea and an example from the book Server Side with Vapor (by the Raywenderlich.com Tutorial Team) I've created this custom middleware that makes the work:
final class SecretMiddleware: Middleware {
let secret: String
init(secret: String) {
self.secret = secret
}
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
guard let bearerAuthorization = request.http.headers.bearerAuthorization else {
throw Abort(.unauthorized, reason: "Missing token")
}
guard bearerAuthorization.token == secret else {
throw Abort(.unauthorized, reason: "Wrong token")
}
return try next.respond(to: request)
}
}
extension SecretMiddleware: ServiceType {
static func makeService(for worker: Container) throws -> SecretMiddleware {
let secret: String
switch worker.environment {
case .development:
secret = "foo"
default:
guard let envSecret = Environment.get("SECRET") else {
let reason = "No SECRET set on environment."
throw Abort(.internalServerError, reason: reason)
}
secret = envSecret
}
return SecretMiddleware(secret: secret)
}
}
Related
I've been working with the Amplify SDK to get federatedSignIn working with my iOS app with "Sign in with Apple" and Cognito to eventually make calls to API Gateway / Lambda functions.
TL;DR : My access token does not appear to be "automatically included in outbound requests" to my API as per the last paragraph of this section of the docs : Cognito User pool authorization
I have successfully authenticated using the tutorial found here Authentication Getting Started and other various Youtube videos on the Amazon Web Services channel.
Upon successful sign in through Apple I'm given an ASAuthorizationAppleIDCredential object. This contains the user field (token) which I pass to the Amplify.Auth class using the following Swift code :
func signIn (with userId: String)
{
guard
let plugin = try? Amplify.Auth.getPlugin(for: AWSCognitoAuthPlugin().key),
let authPlugin = plugin as? AWSCognitoAuthPlugin,
case .awsMobileClient (let client) = authPlugin.getEscapeHatch()
else
{
return
}
client.federatedSignIn(providerName: AuthProvider.signInWithApple.rawValue, token: userId) { (state, error) in
if let unwrappedError = error
{
print (unwrappedError)
}
else if let unwrappedState = state
{
print ("Successful federated sign in:", unwrappedState)
}
}
}
All appears to be successful and to double check I use the following bit of code to ensure I'm authorized :
func getCredentialsState (for userId:String)
{
let provider = ASAuthorizationAppleIDProvider()
provider.getCredentialState(forUserID: userId) { (credentialsState, error) in
if let unwrappedError = error
{
print (unwrappedError)
}
switch credentialsState
{
case .authorized:
print ("User Authorized")
case .notFound, .revoked:
print ("User Unauthenticated")
case .transferred:
print ("User Needs Transfer")
#unknown default:
print ("User Handle new use cases")
}
}
}
In the console I see "User Authorized" so everything appears to be working well.
However when I then go to make a call to Amplify.API.post I get the following error:
[Amplify] AWSMobileClient Event listener - signedOutFederatedTokensInvalid
Failed APIError: Failed to retrieve authorization token.
Caused by:
AuthError: Session expired could not fetch cognito tokens
Recovery suggestion: Invoke Auth.signIn to re-authenticate the user
My function for doing the POST is as follows :
func postTest ()
{
let message = #"{'message": "my Test"}"#
let request = RESTRequest (path: "/test", body: message.data(using: .utf8))
Amplify.API.post (request:request)
{
result in switch result
{
case .success(let data):
let str = String (decoding: data, as: UTF8.self)
print ("Success \(str)")
case .failure(let apiError):
print ("Failed", apiError)
}
}
}`
I then went into the API Gateway UI and changed the generated Method Request on my resource from AWS IAM to my Cognito User Pool Authorizer thinking this was the issue. I also changed the awsAPIPlugin authorizationType to "AMAZON_COGNITO_USER_POOLS" in my amplifyconfiguration.json file. This unfortunately did not have any affect.
I've seen posts such as this issue User is not created in Cognito User pool for users logging in with Google federated login #1937 where people discuss the problem of having to to use a web ui to bring up the social sign in. I understand that Apple will reject your app sometimes for this. Therefore this is not a solution.
I then found this post which seems to resolve the issue however this appears to use the old version of the SDK? Get JWT Token using federatedSignIn #1276
I'm not great with Swift (I'm still an Objective C expert, but am slowly learning Swift) so I'm uncertain which path to go here and whether this is actually a solution? It does seem to be quite more complicated than the function I have that does my POST? The RESTRequest does seem to be a simple and easy solution but I'm uncertain how to pass it the Authorization token (or even how to get the token if it is needed here).
However, everything I've read about the SDK is that the authorization should be handled automatically in the background according the docs in my first link above. Specifically pointed out, again, here : Cognito User pool authorization. The last paragraph here states 👍
With this configuration, your access token will automatically be included in outbound requests to your API, as an Authorization header.
Therefore, what am I missing here as this does not appear to automatically include my access token to my outbound requests to my API?
Is is possible at this point to integrate an IOS app with an ERC20 token on the ethereum network.
There seems to be a library called web3.swift that allows for integration with Ethereum. Maybe this is the way to go but don't know if it is ready for a production app.
In addition, there seem to be some online courses on Swift and blockchain such as this one from Lynda and this one from Udemy and some tutorials on blockchain integration such as this from AppCoda and this one which uses the Tierion blockchain as a service. In fact, AWS, Azure and so forth all seem to offer blockchain as a service. Amazon offers managed blockchain with Ethereum.
However, I haven't seen anything that specifically addresses how to integrate an IOS app with Ethereum. And if it's done on the back end by AWS does this defeat the purpose of using a blockchain to avoid centralization on a server?
The use case I am examining would be to view the number of tokens you have and enable users to spend tokens on services. The tokens would reside however on a blockchain.
Thanks for any advice on whether this is even possible at this stage and, if so, how to approach developing it.
New transaction
Infura service might help here. Once you create your account and setup a project - you will be provided with nodes (for main-net and some test-nets) that can perform write operations on a specific Ethereum network.
Here is the code written in Swift and Web3.swift that might help:
func send(sender: String,
receiver: String,
senderSecret: String,
tokenContractAddress: String,
amount: Int,
completion: #escaping Result<Transaction, Error>) {
let web3 = Web3(rpcURL: "YOUR_INFURA_NODE_ID_GOES_HERE")
do {
let privateKey = try EthereumPrivateKey(hexPrivateKey: senderSecret)
let senderWallet = try EthereumAddress(hex: sender, eip55: true)
let receiverWallet = try EthereumAddress(hex: receiver, eip55: true)
let contract = web3.eth.Contract(
type: GenericERC20Contract.self,
address: try EthereumAddress(hex: tokenContractAddress, eip55: true)
)
firstly {
return web3.eth.getTransactionCount(address: privateKey.address, block: .latest)
}.compactMap { nonce in
guard let tx = contract
.transfer(to: receiverWallet, value: BigUInt(amount))
.createTransaction(
nonce: nonce,
from: senderWallet,
value: 0,
gas: 100000,
gasPrice: EthereumQuantity(quantity: 21.gwei)
) else { return nil }
return try tx.sign(with: privateKey, chainId: 3)
}.then { tx in
return web3.eth.sendRawTransaction(transaction: tx!)
}.done { hash in
let tx = Transaction(hash: hash)
completion.set(.success(tx))
}.catch { error in
completion.set(.failure(error))
}
} catch {
completion.set(.failure(error))
}
}
Open information
If there is no need to initiate transactions and you just want to work with public information like token supply/holder balances/etc. - you can try some open API like blockscout to get the data you need.
While following this tutorial: https://medium.com/#pallavtrivedi03/integrating-dialogflow-as-a-chat-bot-in-an-ios-app-e66a4c7f2723
I was unable to find out how to interact with dialogflow intents that require authentication via the swift app. Once the dialogflow path reaches a point where it needs authentication, the request is not met with a response.
I have successfully sent requests and received responses from dialogflow using the following:
func performQuery(senderId:String,name:String,text:String)
{
let request = ApiAI.shared().textRequest()
if text != "" {
request?.query = text
} else {
return
}
//print("request: ")
request?.setMappedCompletionBlockSuccess({ (request, response) in
let response = response as! AIResponse
print(response.result.parameters)
//print(response.result.action)
I am not an experienced swift user so it could be that I am missing something simple, however, I am unable to find any documentation on the topic.
Any information on ways to interact with dialogflow using oauth2 would be greatly appreciated.
My iOS app checks the iCloud account status and then requests an iCloud WebToken using the following method:
#objc static func fetchWebAuthToken ( _ apiToken : String, _ callback : #escaping CCallbackFunctionWithBoolAndString )
{
let fetchAuthorization = CKFetchWebAuthTokenOperation(apiToken: apiToken)
fetchAuthorization.fetchWebAuthTokenCompletionBlock = { webToken, error in
guard let webToken = webToken, error == nil else {
callback ( false, "[SWIFT] fetchWebAuthToken() error. " + (error?.localizedDescription ?? ""));
return;
}
let encodedWebToken = token.addingPercentEncoding (
withAllowedCharacters: CharacterSet(charactersIn: "+/=").inverted
) ?? token
callback ( true, encodedWebToken );
return;
}
CKContainer.default().privateCloudDatabase.add(fetchAuthorization);
}
Everything works correctly and a properly formatted web token is returned.
I then take that web token and, using Postman, I form the request (with exact values removed):
https://api.apple-cloudkit.com/database/1/iCloud.com.[my container]/development/private/users/caller?ckAPIToken=[development container token]&ckWebAuthToken=[web token]
The response is:
{
"uuid": "[abc]",
"serverErrorCode": "ACCESS_DENIED",
"reason": "private db access disabled for this account"
}
If I request to the public database instead, I get a valid and correct response:
https://api.apple-cloudkit.com/database/1/iCloud.com.[my container]/development/public/users/caller?ckAPIToken=[development container token]&ckWebAuthToken=[web token]
{
"userRecordName": "_[user id]",
"nameComponents": {
"givenName": "[First Name]",
"familyName": "[Surname]"
}
}
So, there's two questions here.
1) If I'm requesting a web token in code for the private database, why is it only allowing me to interact with the public database? It feels like it's providing a web token that's only valid for the public database, regardless of the database I add the action to.
2) What are the security implications of validating a user against the public database like this? The token should expire in 30 minutes, which helps from that front.
To prove that a web token works against the private database, I updated "Sign In Callback" int the Dashboard, copied the resulting ckWebAuthToken and was able to get access to the private database through PostMan, so there's no issue from that end. It seems as if the issue lies entirely with the web token returned from the iOS code.
My guess is that it's because the Users record type in CloudKit is always stored in the public database in every CloudKit container.
There shouldn't be any security risks with this validation against the public databse. In my opinion, Apple shouldn't have ever named it "public" because it's not really public. It's just generally available to the users of the app, but only the application and authenticated users can transact with the database as defined by the developer. It's not available to the public.
I'm going to assume you are doing something fancy with this authentication flow, since authenticating a user on an iOS device doesn't require passing around the ckWebAuthToken. :)
I'm just exploring using Alamofire and it is excellent but I'd like to do something that I feel is possible just not sure how.
Our authentication with the server uses one-time-use bearer tokens. So for every request made I have to store the new token sent down with that request.
What I'd like to do is intercept every response that comes back and check the Authorisation header. Save it to disk and then forward to the place waiting for the actual data.
Is this possible with Alamofire?
If so, please could you point me in the right direction.
Thanks
OK, after a bit of searching the github and head scratching I decided to create a new response serialiser by extending the Request type.
I created a new saveAuth() block like so...
extension Request {
public static func AuthSaver() -> ResponseSerializer<Bool, NSError> {
return ResponseSerializer { request, response, data, error in
guard error == nil else { return .Failure(error!) }
if let auth = response?.allHeaderFields["Authorization"] as? String {
Router.OAuthToken = auth // this uses a didset on the Router to save to keychain
}
return .Success(true)
}
}
public func saveAuth() -> Self {
return response(responseSerializer: Request.AuthSaver()) {_ in}
}
}
I can call it like...
Alamofire.request(Router.Search(query: query))
.validate()
.responseSaveAuth() // this line
.responseJSON {
response in
// ...
}
It still requires adding in each place that I want to strip out the newly sent auth token but it means I can choose when not to do it also and it's a single line of code.
It's maybe not the most elegant code in the extension (I'm still getting to grips with it all) but it makes it much easier to save the authentication each time.
I have solved this by only having one place in my app that sends network requests. Basically, I have a "network manager" that builds up NSURLRequests and pipes them to one function that actually sends the request (in my case it's an NSOperation sub class). That way I have only one location that I'm reading responses from.