How do I generate a JWT to use in API authentication for Swift app - swift

I am trying to implement the use of an API service which requires JWT authentication for all its API calls.
I understand what JWT tokens are and how they are used, my issue is that I am writing a Swift app and can't quite figure out the process to generate the token so that I can attach it as a Bearer in my API calls.
Can I generate the JWT token on the client side (swift app)?
Create a Google Cloud Function to generate token then write back to Firebase
to use in my API calls?

Here's how to make JSON Web Tokens in Swift using Apple's CryptoKit. It uses the default example in https://jwt.io
import CryptoKit
extension Data {
func urlSafeBase64EncodedString() -> String {
return base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
struct Header: Encodable {
let alg = "HS256"
let typ = "JWT"
}
struct Payload: Encodable {
let sub = "1234567890"
let name = "John Doe"
let iat = 1516239022
}
let secret = "your-256-bit-secret"
let privateKey = SymmetricKey(data: Data(secret.utf8))
let headerJSONData = try! JSONEncoder().encode(Header())
let headerBase64String = headerJSONData.urlSafeBase64EncodedString()
let payloadJSONData = try! JSONEncoder().encode(Payload())
let payloadBase64String = payloadJSONData.urlSafeBase64EncodedString()
let toSign = Data((headerBase64String + "." + payloadBase64String).utf8)
let signature = HMAC<SHA256>.authenticationCode(for: toSign, using: privateKey)
let signatureBase64String = Data(signature).urlSafeBase64EncodedString()
let token = [headerBase64String, payloadBase64String, signatureBase64String].joined(separator: ".")
print(token) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It depends on how you plan to sign your token. Fundamentally, you'll want some sort of secret to sign the payload of the JWT.
Is your secret an API key that the client already has? If so, there's not a lot of harm just generating it client side.
Is your secret a certificate that's super secret and you can't give out to clients? Then you'll probably want to go with your Firebase idea.
It's pretty common to just have the client do the signing via API key in these situations, but your motivations for locking down your API to begin with are the driving force here.
IBM-Swift looks like the most complete JWT library for swift these days should you decide to go client side.
Jsonwebtoken is a very good JS one should you decide to deploy a GC Function.
Both libraries are very straightforward to use.

Related

Downloading files with a manually generated signed url errors with "SignatureDoesNotMatch"

I've followed this guide to implement a URL signer for cloud storage download URLs in Rust.
Bucket and service account have necessary permissions.
I'm using signBlob technique to sign the string.
Here is the output of my canonical request.
GET
/abc.pd
x-goog-algorithm=GOOG4%2DRSA%2DSHA256&x-goog-credential=basak%2Dservice%40alibasak%2Eiam%2Egserviceaccount%2Ecom%2F20220521%2Fauto%2Fstorage%2Fgoog4%5Frequest&x-goog-date=20220521T004156Z&x-goog-expires=3600&x-goog-signedHeaders=host
host:basak.storage.googleapis.com
host
UNSIGNED-PAYLOAD
My string to sign.
(Hash is calculated from the canonical request with SHA256)
GOOG4-RSA-SHA256
20220521T004156Z
20220521/auto/storage/goog4_request
43b27d5947adf3b915d8a5a51cfe2f5cf1344a12b3d3731287cf4741525eabef
The final signed url I produce.
https://basak.storage.googleapis.com/abc.pd?x-goog-algorithm=GOOG4%2DRSA%2DSHA256&x-goog-credential=basak%2Dservice%40alibasak%2Eiam%2Egserviceaccount%2Ecom%2F20220521%2Fauto%2Fstorage%2Fgoog4%5Frequest&x-goog-date=20220521T004156Z&x-goog-expires=3600&x-goog-signedHeaders=host&x-goog-signature=TBqhQ9edLBnGC0z8jhWPAt6NDGM87PHcdAZBt2bcVfd9N/zE1i/HY0jUi5XZoMUgABoBvU36dizS4lr8PrOjXG6GT9KgXbEBcrQqPb83outeAfhL2pgXgbQjXcetFX7cYzY3GSULRWs7+7wH0rxMWiQ6E3tahraBUXI9VZ2XqbUGLuZZXtOhExQ14dKWOnvVVEl0C5BehMEXpDzMFXSWUrsuDMpDlN86nwaJgcGlTNBBrot7J2gMde+xGcJ4zC/c3BADoKHGdjhyOzQh7zToQHnpkLHdEVILUD7k4CN6f9TNzvUGsqNABJ3H4t3fwDgZ/2OqSJ9Na6Xisi2OMMgaTQ==
Trying a get request with this URL results with this error,
<?xml version='1.0' encoding='UTF-8'?>
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message>
<StringToSign>GOOG4-RSA-SHA256
20220521T004156Z
20220521/auto/storage/goog4_request
e38b8ab7532e4fa3009a7234313086ed45551be91398a5eef856eef4bfd857cf</StringToSign>
<CanonicalRequest>GET /abc.pd x-goog-algorithm=GOOG4-RSA-SHA256&x-goog-credential=basak-service%40alibasak.iam.gserviceaccount.com%2F20220521%2Fauto%2Fstorage%2Fgoog4_request&x-goog-date=20220521T004156Z&x-goog-expires=3600&x-goog-signedHeaders=host
host:basak.storage.googleapis.com
host
UNSIGNED-PAYLOAD</CanonicalRequest>
</Error>
I've read that sometimes a header mismatch was causing this, I've tried to make this request in browser, curl and with a node script. Each of those give me the same error.
Could you point me to the right direction about debugging this or maybe you may think of causes which I'm not aware yet.
Thanks a lot.
Update
John Hanley suggested that the problem might be in the encoding.
I've applied the fixes he suggested such as sorting the headers and took the example in the end of this page as a reference to percent encode the URL.
Now the canonical request looks like this
GET
/abc.pd
X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=basak-service%40alibasak.iam.gserviceaccount.com%2F20220521%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220521T082931Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host
host:basak.storage.googleapis.com
host
UNSIGNED-PAYLOAD
and the final URL looks like this,
https://basak.storage.googleapis.com/abc.pd?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=basak-service%40alibasak.iam.gserviceaccount.com%2F20220521%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220521T082931Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=SEf8qecHjF+T+Uy71jygrEzrj2I1TXeKdUAOpm+BEf0Xm0W9e/LH5MC5BN+JA+OZ44EHN///Ai8ZO7GXqmE6+xbjrVhPlVETkLQSQ84GVecxj6onauRmpDTpxW8RXc+fXdzia75YndT5D1HKjvy8nI26Va8YMG2U3W/A7HMjg5YG+Cmcow3Pw1jpjIG+03gLLobhVMTnyp63S5AnycWT3Wzf7uo6l/WR7MxMK7pKA3isXXhQg7g9o8XUFFMvesDZUsI6mLYakxFHKHL42p1h6/P1SCFke1lpuYa9pV/EVnUMVfLp1ZfkgA1WBLqKZzyPNhMepfgIsUiUv2AKx/VoWQ==
I've also used the cli command,
gsutil -i basak-service#alibasak.iam.gserviceaccount.com signurl -d 10m -u gs://basak/abc.pd
to sign the url for comparison and the result is
https://storage.googleapis.com/basak/abc.pd?x-goog-signature=454801bc0d9ae19b5c5465a4e76846abbb1775549fe2532c839952125a54c5b1cb8f89385ac1cc1a8313e23945951d259a2ddc5a1c95890a205db1ab30a32d6efe8a2e706d03c68de6c4a502f50ff1a1b7fae5b94e3aa85768bfe473abf557eb8ae4e2b15ff9a5bb73ccb3d0bd1b8470cbe0bcb7ec6538fc575664672d641cb9f3c63ec04c41c13a6f2f6329290ce82bc57a700137edcf6fbade0885dd8130ebe2ba9bfe48f91ec94bf6e85b2ac8a7a26aeda77cbd5b0c30136d77defffeb5493f08bf9479f84522c1cb78503693e8ceab79fe0c6282ac4ecaa7e33b6355d2a7f870409b777512819ef54628a86a43b14ce8370477d11a9f857c2ec4ada90d6b&x-goog-algorithm=GOOG4-RSA-SHA256&x-goog-credential=basak-service%40alibasak.iam.gserviceaccount.com%2F20220521%2Fus%2Fstorage%2Fgoog4_request&x-goog-date=20220521T090829Z&x-goog-expires=600&x-goog-signedheaders=host
this url works perfectly.
The differences are:
Header order starts with the signature and continues sorted.
Headers are lowercase
The signature does not contain slashes. (It could be that I need to decode the signature? I'll check.)
I've implemented the first two in my code quickly and nothing changed.
On the other hand I suspect that maybe I'm doing something wrong with the signature since neither the example nor the gsutil generated url have slashes in the signature..
As John Hanley suggested I am sharing my code.
(The code is written in a sketchy way to debug and make it work first. Imports and use statements omitted.)
const SERVICE_ACCOUNT_EMAIL: &str = "basak-service#alibasak.iam.gserviceaccount.com";
#[derive(Serialize)]
struct SignRequest {
// The sequence of service accounts in a delegation chain. Each service account must be granted the roles/iam.serviceAccountTokenCreator role on its next service account in the chain. The last service account in the chain must be granted the roles/iam.serviceAccountTokenCreator role on the service account that is specified in the name field of the request.
// The delegates must have the following format: projects/-/serviceAccounts/{ACCOUNT_EMAIL_OR_UNIQUEID}. The - wildcard character is required; replacing it with a project ID is invalid.
delegates: Vec<String>,
// Required. The bytes to sign.
// A base64-encoded string.
payload: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SignResponse {
key_id: String,
signed_blob: String,
}
fn credential_scope(date: &str, location: &str, service: &str, request_type: &str) -> String {
format!("{date}/{location}/{service}/{request_type}")
}
const FRAGMENT: &AsciiSet = &CONTROLS
// ?=!#$&'()*+,:;#[]."
.add(b'/')
.add(b' ')
.remove(b'=')
.remove(b'!')
.remove(b'#')
.remove(b'$')
.remove(b'&')
.remove(b'\'')
.remove(b'(')
.remove(b')')
.remove(b'*')
.remove(b'+')
.remove(b',')
.remove(b':')
.remove(b';')
.add(b'#')
.remove(b'[')
.remove(b']')
.remove(b'.')
.remove(b'"');
async fn generate_signed_url(
bucket_name: &str,
object_name: &str,
expiration: &str,
token: &gcp_auth::Token,
) -> Result<String, reqwest::Error> {
// We'll use google's signing service to generate a signature.
let sign_blob_url = format!("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{SERVICE_ACCOUNT_EMAIL}:signBlob?alt=json");
let mut query_parameters = std::collections::BTreeMap::new();
let canonical_uri = format!("/{object_name}");
let date = chrono::offset::Utc::today().format("%Y%m%d").to_string();
let time_stamp = chrono::offset::Utc::now()
.format("%Y%m%dT%H%M%SZ")
.to_string();
let credential_scope = credential_scope(&date, "auto", "storage", "goog4_request");
let credential = format!("{SERVICE_ACCOUNT_EMAIL}/{credential_scope}");
let host = format!("{bucket_name}.storage.googleapis.com");
let algorithm = "GOOG4-RSA-SHA256";
query_parameters.insert("x-goog-algorithm", algorithm);
query_parameters.insert("x-goog-credential", &credential);
query_parameters.insert("x-goog-date", &time_stamp);
query_parameters.insert("x-goog-expires", expiration);
query_parameters.insert("x-goog-signedHeaders", "host");
let mut canonical_query_string =
query_parameters
.iter()
.fold("".to_owned(), |mut acc, (k, v)| {
//
let encoded_k = percent_encode(k.as_bytes(), FRAGMENT);
let encoded_v = percent_encode(v.as_bytes(), FRAGMENT);
acc.push_str(&format!("{}={}&", encoded_k, encoded_v));
acc
});
canonical_query_string.pop();
// let canonical_headers = format!("content-type:text/plain\nhost:{host}");
// let signed_headers = "content-type;host";
let canonical_headers = format!("host:{host}");
let signed_headers = "host";
// HTTP_VERB
// PATH_TO_RESOURCE
// CANONICAL_QUERY_STRING
// CANONICAL_HEADERS
// \n
// SIGNED_HEADERS
// PAYLOAD
let canonical_request = format!(
"GET\n{canonical_uri}\n{canonical_query_string}\n{canonical_headers}\n\n{signed_headers}\nUNSIGNED-PAYLOAD"
);
println!("canonical_request: {}", canonical_request);
let mut hasher = Sha256::new();
hasher.update(canonical_request);
let hashed_canonical_request = format!("{:x}", hasher.finalize());
let string_to_sign =
format!("{algorithm}\n{time_stamp}\n{credential_scope}\n{hashed_canonical_request}");
println!("string_to_sign: {}", string_to_sign);
let body = SignRequest {
delegates: vec![],
payload: base64::encode(string_to_sign),
};
let client = reqwest::Client::new();
let response = client
.post(sign_blob_url)
.bearer_auth(token.as_str())
.json(&body)
.send()
.await?
.bytes()
.await?;
dbg!(&response);
let sign_response: SignResponse = serde_json::from_slice(&response).unwrap();
let signed_url = format!(
"https://{host}{canonical_uri}?x-goog-signature={}&{canonical_query_string}",
// percent_encode(response.signed_blob.as_bytes(), FRAGMENT)
sign_response.signed_blob
);
Ok(signed_url)
}
First of all big thanks to John Hanley for motivating me to resolve this.
As he suggested there were multiple issues.
The percent encoding was incorrect and comparing it with the CLI output helped me to correct it.
The headers should be sorted and the same technique of comparing also helped me to find the right sorting.
The major issue was something else though.
It is my mistake that I've missed this in the docs.
In the doc of signedBlob it clearly states that the signature in the response is a Base64 encoded string. I was using it plainly instead.
After receiving the response doing,
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SignResponse {
key_id: String,
signed_blob: String,
}
let response: SignResponse = serde_json::from_slice(&response).unwrap();
// First decode the signature
let decoded_signature = base64::decode(response.signed_blob).unwrap();
// Then make a string with hex representation of bytes
let hex_encoded_signature = hex::encode(decoded_signature);
// Then construct the url
let signed_url = format!("https://{host}{canonical_uri}?x-goog-signature={hex_encoded_signature}&{canonical_query_string}");
resolved the issue and I can even use the resulting URL in the browser!
By experimentation I've also derived that using lowercase (x-goog-..) or uppercase (X-Goog..) headers or even mixing them do not change anything.
This comment also confused me a little throughout the process:
When defining the resource path, you must percent encode the following reserved characters: ?=!#$&'()*+,:;#[]." Any other percent encoding used in the URL should also be included in the resource path.
which is taken from this source
Because it says to percent encode that character set but does not include characters such as /, etc. which should be percent encoded also and such as . shouldn't be encoded.
I am still confused about it a little but maybe I should read more about percent encoding techniques.
Anyway I'm happy that it is resolved and hope it helps someone later.

AWSS3TransferUtility uploading with Cognito authentication configuration issues?

I have an app that authenticates with Cognito and has been working fine.
Now I need to reuse the authentication and returned token to upload files to S3. My understanding is that AWSS3TransferUtility is the way to go at this point. Only it is not clear what needs to be done and how is the token to be passed to S3? Can anyone point to an example? Just using examples available suggesting to do:
let credentialsProvider =
CredentialsProvider(regionType:region, identityPoolId:poolId)
let serviceS3Configuration = AWSServiceConfiguration(region:region, credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = serviceS3Configuration
results in "Unauthenticated access is not supported for this identity pool" assuming that this is because token is not being used and S3 AWS service is not initialized correctly. But I do not see a way to set it ? what am I missing? I can see some examples suggesting setting logins property of credentialsProvider to
AWSCognitoLoginProviderKey but seems to be outdated at this point.
Any and all help would be greatly appreciated.
S3 configuration with Cognito is a little mysterious. The answers are in the docs but not entirely obvious. The core of getting this to work is registering your AWSMobileClient instance with the configuration.
Simplified code without error checking:
Boot up your AWSMobileClient:
AWSMobileClient.sharedInstance().initialize({ { userstate, error in
if userstate != nil {
registerAuthentication(credentialsProvider: AWSMobileClient.sharedInstance())
}
})
Once completed pass the sharedInstance to AWSServiceConfiguration because AWSMobileClient is-a AWSCredentialsProvider
let DefaultTransferUtilityKey = "DEFAULT_AUTH_KEY"
func registerAuthentication(credentialsProvider: AWSCredentialsProvider) {
/// only do this once per app launch
/// assumes you're using the plist config method
guard let s3tranferInfo = AWSInfo.default().defaultServiceInfo("S3TransferUtility"),
let bucketName = s3tranferInfo.infoDictionary["Bucket"] as? String else {
assertionFailure("failed to load /S3TransferUtility/Bucket key - is awsconfiguration.json correct ?")
return
}
let transferConfig = AWSS3TransferUtilityConfiguration()
transferConfig.bucket = bucketName
if let serviceconfiguration = AWSServiceConfiguration(region: s3tranferInfo.region, credentialsProvider: credentialsProvider) {
AWSS3TransferUtility.register(with: serviceconfiguration, transferUtilityConfiguration: transferConfig, forKey: DefaultTransferUtilityKey)
}
}
and once that registration is actually finished you can access the transfer utility via the common key string.
lazy var transferUtility: AWSS3TransferUtility = {
let utility = AWSS3TransferUtility.s3TransferUtility(forKey: DefaultTransferUtilityKey)
return utility
}()
Bucket name and region could be strings also but if you're using AWSMobileClient you probably have the plist setup.

Swift Vapor unsupported_grant_type invalid signature / OAuth access token

I am running Xcode 8.1 with Vapor and SWIFT 3.
I am posting a request to to google server to get an auth token, so I can call FireBaseDB API, but I get error: unsupported_grant_type/Invalid grant_type.
On developers.google.com it says that I have to encode in a URL the following: https://www.googleapis.com/oauth2/v4/token + grant_type + assertion, and pass the encoded URL in the body of the POST request. I pass it as a string.
I have noticed that the private key from the JSON file downloaded from my service account contains characters such as /n , ----,==, should I delete them before posting the key?
let dateNow = Date()
var expDate = String(Int(dateNow.timeIntervalSince1970 + (60 * 60)))
var iatDate = String(Int(dateNow.timeIntervalSince1970))
let headerJWT = ["alg":"HS256","typ":"JWT"]
let jwtClaimSet =
["iss":"firebase-adminsdk-c7i48#fir-10c2e.iam.gserviceaccount.com",
"scope":"https://www.googleapis.com/auth/firebase.database",
"aud":"https://www.googleapis.com/oauth2/v4/token",
"exp": expDate,
"iat": iatDate]
//create and sign JSON Web Token
let jwt = try JWT(headers: Node(node: headerJWT),
payload: Node(node: jwtClaimSet),
signer: HS256(key:"-----BEGIN PRIVATE KEY-----\nMIIEvAWvQ== \n-----END PRIVATE KEY-----\n"))
// Store JSON Web Token
let JWTtoken = try jwt.createToken()
func createUrlWithString() -> NSURL {
var urlString = "https://www.googleapis.com/oauth2/v4/token"
urlString.append("?grant_type=")
urlString.append("urn:ietf:params:oauth:grant-type:jwt-bearer")
urlString.append("&assertion=")
urlString.append(JWTtoken)
return NSURL(string: urlString)!
}
// make the body input for our POST
let bodyURL = createUrlWithString().absoluteURL
drop.get("any") { request in
let response =
try drop.client.request(.other(method:"Post"),
"https://www.googleapis.com/oauth2/v4/token",
headers: ["Content-Type": "application/x-www-form-urlencoded"],
query: [:],
body: String(describing: bodyURL) )
let serverResp = response.headers
let serverBody = response.body.bytes
let serverJson = try JSON(bytes: serverBody!)
print(serverJson)
return "POST Request went through"
}
Update
As per Karol Gasienica suggestion, I am passing grant_type and assertion parameters as POST request parameters. Now I get "error_description": Node.Node.string("SSL is required to perform this operation.")]))
func createUrlWithString() -> NSURL {
var urlString = "https://www.googleapis.com/oauth2/v4/token"
urlString.append("?grant_type=")
urlString.append("urn:ietf:params:oauth:grant-type:jwt-bearer")
urlString.append("&assertion=")
urlString.append(JWTtoken)
return NSURL(string: urlString)!
}
let response = try drop.client.request(.other(method:"Post"),
String(describing: bodyURL),
headers: ["Content-Type": "application/x-www-form-urlencoded"],
query: [:])
When you generate a service account credential you need to have in mind the following, taken from https://cloud.google.com/storage/docs/authentication:
You can create a private key in the Cloud Platform Console by creating an OAuth Client ID for a service account. You can get your private key in JSON and PKCS12 format:
JSON keys are required if you are using Application Default Credentials in a production environment outside of Google Cloud Platform. JSON keys cannot be converted to other formats.
PKCS12 (.p12) is supported by many different programming languages and libraries. If needed, you can convert the key into other formats using OpenSSL (see Converting the private key to other formats). However, PKCS12 keys cannot be converted to JSON format.
Create your service account and then download your .p12 file.
Convert the p.12 (a.k.a pkcs12) file to .pem (a.k.a pkcs1) using OpenSSL
cat /path/to/xxxx-privatekey.p12 | openssl pkcs12 -nodes -nocerts -passin pass:notasecret | openssl rsa > /path/to/secret.pem
Go to github and search VaporJWT and import it in Xcode. It will help you create a signed JSON Web Token.
On this github page you will learn how to extract the private key for RSA use.
// convert .pem to der
openssl rsa -in /path/to/secret.pem -outform der -out /path/to/private.der
//convert .der to .base64
openssl base64 -in /path/to/private.der -out /path/to/Desktop/private.txt
In private.txt you have the private key encoded in base64 which you can finally use to sign you JWT. Then you can make calls to Google API with the signed JWT.
import Vapor
import VaporJWT
let drop = Droplet()
var tokenID:String!
//set current date
let dateNow = Date()
// assign to expDate the validity period of the token returnd by OAuth server (3600 seconds)
var expDate = String(Int(dateNow.timeIntervalSince1970 + (60 * 60)))
// assign to iatDate the time when the call was made to request an access token
var iatDate = String(Int(dateNow.timeIntervalSince1970))
// the header of the JSON Web Token (first part of the JWT)
let headerJWT = ["alg":"RS256","typ":"JWT"]
// the claim set of the JSON Web Token
let jwtClaimSet =
["iss":"firebase-adminsdk-c7i38#fir-30c9e.iam.gserviceaccount.com",
"scope":"https://www.googleapis.com/auth/firebase.database",
"aud":"https://www.googleapis.com/oauth2/v4/token",
"exp": expDate,
"iat": iatDate]
//Using VaporJWT construct a JSON Web Token and sign it with RS256 algorithm
//The only signing algorithm supported by the Google OAuth 2.0 Authorization Server is RSA using SHA-256 hashing algorithm.
let jwt = try JWT(headers: Node(node: headerJWT), payload: Node(node:jwtClaimSet), encoding: Base64URLEncoding(), signer: RS256(encodedKey: "copy paste here what you have in private.txt as explained at point 7 above "))
// create the JSON Web Token
let JWTtoken = try jwt.createToken()
let grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" // this value must not be changed
let unreserved = "*-._"
let allowed = NSMutableCharacterSet.alphanumeric()
allowed.addCharacters(in: unreserved)
// percent or URL encode grant_type
let grant_URLEncoded = grant_type.addingPercentEncoding(withAllowedCharacters: allowed as CharacterSet)
// create a string made of grant_type and assertion. NOTE!!! only grant_type's value is URL encoded.
//JSON Web Token value does not need to be URL encoded
var fullString = "grant_type=\(grant_URLEncoded!)&assertion=\(JWTtoken)"
//pass fullString in the body parameter
drop.get("call") { request in
let response = try drop.client.post("https://www.googleapis.com/oauth2/v4/token", headers: ["Content-Type": "application/x-www-form-urlencoded"], query: [:],body: fullString)
let serverResp = response.headers
let serverBody = response.body.bytes
let serverJson = try JSON(bytes: serverBody!)
print(serverJson)
return "Success"
}
It seems like you dont set properly grant_type in your code:
urlString.append("?grant_type=")
In your case it might be grant_type=authorization_code
or
grant_type=jwt-bearer
It seems like you set grant_type in wrong place.
Update
Also I think grant_type and assertion parameters are not passed as request headers but as post request parameters
Update
Im not really sure you are using correct way to put POST (body) parameters. In this documentation is example how to create request with post prams and it looks like follows:
try drop.client.request(.other(method: "POST"), "http://some-domain", headers: ["My": "Header"], query: ["key": "value"], body: [])

Validate AWS Cognito Token with Swift

I have nearly completed the process for a developer authenticated sign in using AWS. I cannot seem to authenticate the back-end token that I receive and cannot seem to find any current implementations that are performing developer authentication via a third-party back-end. The error that I get is listed below.
As of right now my code looks like this:
Class containing Custom identity provider:
import Foundation
import AWSCognitoIdentityProvider
class CustomIdentityProvider: NSObject, AWSIdentityProviderManager {
var tokens: [NSString: NSString]?
init(tokens: [NSString: NSString]) {
self.tokens = tokens
}
#objc func logins() -> AWSTask<NSDictionary> {
return AWSTask(result: tokens! as NSDictionary)
}
}
AWS-APIManager.swift {snippet}
/* obtained cognito token from my back-end via getOpenIdTokenForDeveloperIdentity*/
/* From here I my app receives an IdentityId and Token */
let client_cognito_id = String(describing: valid_response)
let session_token = json.dictionaryValue["Token"]!
let login_with_amazon = NSString(string: "cognito-identity.amazonaws.com")
let token = NSString(string: String(describing: session_token))
let customProviderManager = CustomIdentityProvider(tokens: [login_with_amazon: token])
let credentialsProvider = AWSCognitoCredentialsProvider(
regionType: self.AWS_REGION,
identityPoolId: "us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
identityProviderManager: customProviderManager
)
credentialsProvider.setIdentityProviderManagerOnce(customProviderManager)
credentialsProvider.getIdentityId().continue ({ (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
print("Error!!!: " + (task.error?.localizedDescription)!)
} else {
// the task result will contain the identity id
let cognitoId = task.result
print(cognitoId)
print("SUCCESS!!!")
}
return nil
})
}
For some odd reason odd reason I can cannot authenticate the token that I have received. I get an error "Invalid login token. Can't pass in a Cognito token.". I've tried to follow the documentation and piece together working code that I have found literally hundreds of sources and cannot seem to be able to move past this part of the authentication process. Any help would be greatly appreciated. Thanks!
I believe the issue here is that although you are supplying the token, you are not setting the identity id that you are getting from your backend. As such, it is calling GetId with a Cognito OpenIdConnectToken, which is not supported.
The simplest client implementation of Developer Authenticated Identities is to extend AWSCognitoCredentialsProviderHelper
Apologies for providing this in Objective C instead of Swift. In your implementation just override the token method.
- (AWSTask<NSString *> *)token {
//get the identity id and token from your server
//You can use AWSTaskCompletionSource if you don't have it and need to get it asynchronously.
//Once you have this information, simply set the identity id and return the token
self.identityId = identityId;
return [AWSTask taskWithResult:token];
}

How to invoke an AWS Lambda function in Swift

I can't find any documentation or examples on how to invoke a Lambda function in Swift but I've tried to extrapolate from the documentation using Objective-C and I'm still getting errors:
"Error in myFunction: ValidationException: Supplied AttributeValue is empty, must contain exactly one of the supported datatypes"
It appears that I'm not passing in the parameters to the function correctly when I invoke the lambda function from swift because the script tries to write to DynamoDB but one of the parameters is empty (this lambda script works when I invoke it in javascript/node).
let lambda = AWSLambda.defaultLambda()
let request = AWSLambdaInvocationRequest()
var context = [String: String]()
let jsonString = "{\"email\":\"example#example.com\",\"name\":\"example\"}"
let jsonData = jsonString.dataUsingEncoding(NSUTF8StringEncoding)
request.clientContext = jsonData?.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
request.functionName = "myFunction"
lambda.invoke(request).continueWithBlock( {
(currentTask: AWSTask!) -> AWSTask in
if (currentTask.error != nil) {
// failed to execute.
print("Error executing: ", currentTask.error)
task.setError(currentTask.error)
} else {
print("token: ", currentTask.result)
task.setResult(currentTask.result)
}
return currentTask
})
You need to set the payload parameter to a map containing the data you want to pass.
let invocationRequest = AWSLambdaInvokerInvocationRequest()
invocationRequest.functionName = "myFunction"
invocationRequest.invocationType = AWSLambdaInvocationType.RequestResponse
invocationRequest.payload = ["email" : "example#example.com", "name" : "example"]
let lambdaInvoker = AWSLambdaInvoker.defaultLambdaInvoker()
let task = lambdaInvoker.invoke(invocationRequest).continueWithSuccessBlock() { (task) -> AWSTask! in
print("response: ", task.result)
}
Ryan Fitzgerald's answer gives me multiple compile-time errors, but I've had success with this version:
First, I have an initialization function with access credentials. Note that this is not the recommended secure access method for production code, but it is fine for testing and other purposes. It also assumes you have a Constants.swift file where you define the listed constants:
func initializeLambda() {
let credentialsProvider = AWSStaticCredentialsProvider.init(accessKey:Constants.AWS_ACCESS_KEY, secretKey: Constants.AWS_SECRET_KEY)
let defaultServiceConfiguration = AWSServiceConfiguration(region: Constants.AWS_REGION, credentialsProvider: credentialsProvider)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = defaultServiceConfiguration
}
For the remainder we can provide a version similar to the previous version. I removed the 'let task' because 'task' is not used in his example. Additionally, I've included the logical outline of some JSON parsing that you are likely to be doing inside the invoke task. Finally, I've changed to a continueWithBlock(). If you use a continueWithSuccessBlock() you will not enter this block when Amazon Lambda reaches its timeout window or if something else goes wrong with the request and typically you do want these situations to be handled here.
self.initializeLambda() //Call our previously written initialization function
let invocationRequest = AWSLambdaInvokerInvocationRequest()
invocationRequest.functionName = "functionName"
invocationRequest.invocationType = AWSLambdaInvocationType.RequestResponse
invocationRequest.payload = ["key1" : "value1", "key2" : "value2"]
let lambdaInvoker = AWSLambdaInvoker.defaultLambdaInvoker()
lambdaInvoker.invoke(invocationRequest).continueWithBlock() { (task: AWSTask) -> AWSTask in
print("response: ", task.result)
//In here you'll likely be parsing a JSON payload
if let payload: AnyObject = task.result?.payload {
if let error: AnyObject = payload.objectForKey("error") {
//If there is an error key in the JSON dictionary...
} else {
//If the JSON dictionary has no error key...
}
return task;
}
}
Tested and verified as functional on Swift 2.2 in Xcode 7.3.
The answers from both the Ryan's were great and useful and I just want to add a couple of additional thoughts.
In most cases, before you can invoke a Lambda, you might need to authenticate, so the errors you get might not necessarily be because of your Lambda calls but due to failing authentication. With AWS, however, there are several different ways to authenticate and this will change based on the credentials you have.
Ryan Davis shows you one way where your backend team has set up an AWS Access Key and an AWS Secret Key.
In my case, I had to authenticate using AWS Cognito Identity Pools and there are also User Pool authentication so you need to figure out what credentials your team has given you and read the appropriate authentication documentation.
Since I needed to use using AWS Cognito Identity Pools, all I had was the region and the identity pool id so in Swift 5 authentication for AWS Cognito Identity Pools
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: Constants.AWS_REGION,
identityPoolId: Constants.AWS_REGION.AWS_IDENTITY_POOL_ID)
let serviceConfiguration = AWSServiceConfiguration(region: Constants.AWS_REGION,
credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = serviceConfiguration
And then the Lambda invocation more or less stays the same but just with slightly updated Swift 5 syntax:
if let invocationRequest = AWSLambdaInvokerInvocationRequest() {
invocationRequest.functionName = "function_name"
invocationRequest.invocationType = AWSLambdaInvocationType.requestResponse
invocationRequest.payload = ["key_1": "value_1"]
let lambdaInvoker = AWSLambdaInvoker.default()
lambdaInvoker.invoke(invocationRequest) { (awsLambdaInvokerInvocationResponse, error) in
guard let payload = awsLambdaInvokerInvocationResponse?.payload as? [String: String] else {
// Handle error here
return
}
if let userId = payload["message"] {
print("USR Id: \(userId)")
}
}
}
You will need to adjust your handling based on the structure of your payload returned to you by the Lambda, in my case it was:
{
"message": "user-id-8868-8475-8757"
}
Finally, remember to import the required libraries for your use case, for my above case I needed:
import AWSCore
import AWSLambda