I'm develop app IOS and Android with Xamarin cross-platform.
I'm trying hard to use credentials receiving from Cognito Identity to authorize app invoking API Gateway.
My user flow is:
Authenticate on Cognito User Pool and get tokens
Exchange tokens for credentials on Cognito Identity Pools
Access API Gateway using credentials retrieved in the previous step.
Step 1 and 2 seems to works fine. But when app try to connect to api gateway it's getting error 403 (Forbidden).
My code:
Authenticate on Cognito User Pool and get tokens
**
public async Task<AppUser> Login(string username, string password)
{
CognitoUser cognitoUser = new CognitoUser(username, Aws.COGNITO_CLIENT_ID, CognitoUserPool, CognitoIdentityProviderClient);
AppUser appUser = new AppUser() { Email = username };
// Send a login request and wait for the response from Amazon
try
{
AuthFlowResponse response = await cognitoUser.StartWithSrpAuthAsync(new InitiateSrpAuthRequest()
{
Password = password
});
;
appUser.IsAuthenticated = true;
}
catch (NotAuthorizedException e)
{
appUser.IsAuthenticated = false;
appUser.ErrorMessage = e.Message;
}
await _tokenManagement.SaveTokens(cognitoUser.SessionTokens) ;
return appUser;
}
Exchange tokens for credentials on Cognito Identity Pools
**
public async Task<ImmutableCredentials> GetAppCredentialsAsync()
{
CognitoAWSCredentials cac;
if (_tokenManagement.CheckIsAnonymous())
{
//Anonymous credentials
cac = new CognitoAWSCredentials(Aws.COGINITO_IDENTITY_POLL_ID, RegionEndpoint.USEast1);
}
else
{
//Retrieve saved tokens from previous authentication
var tm = await _tokenManagement.RetrieveTokens();
CognitoUser user = new CognitoUser(null, Aws.COGNITO_CLIENT_ID, CognitoUserPool, CognitoIdentityProviderClient)
{
SessionTokens = new CognitoUserSession(tm.IdToken, tm.AccessToken, tm.RefreshToken, tm.IssuedTime, tm.ExpirationTime)
};
//Retrieve authenticated credentials
cac = user.GetCognitoAWSCredentials(Aws.COGINITO_IDENTITY_POLL_ID, RegionEndpoint.USEast1);
}
}
return await cac.GetCredentialsAsync();
}
Access API Gateway using credentials retrieved in the previous step:
**
public async Task<IList<MediaImage>> GetCoversAWSAsync()
{
// Getting credentials and sign request
var request = await BuildRequestAsync("/listMedia");
var client = new HttpClient();
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
IList<MediaImage> covers = JsonConvert.DeserializeObject<IList<MediaImage>>(await response.Content.ReadAsStringAsync());
return covers;
}
private async Task<HttpRequestMessage> BuildRequestAsync(string service)
{
var request = new HttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new Uri(baseURL + service)
};
ImmutableCredentials awsCredential = await _loginService.GetAppCredentialsAsync( );
var signer = new AWS4RequestSigner(awsCredential.AccessKey, awsCredential.SecretKey);
request = await signer.Sign(request, "execute-api", awsRegion);
return request;
}
This code works fine when I hard code credentials from one IAM user. But credentials retrieved from Cognito Identities its getting Forbidden error.
I did a test using SDK for S3 and I was able to successfully list the buckets with the same credentials received from Cognito Identities, but it is not possible to make requests on the API Gateway.
Can you help me? Where did I get lost?
I figure out what was going on.
After ensuring that the permissions settings were correct on AWS.
I found in the documentation that it is necessary to include in the HTTP header the token returned by the cognito (header name x-amz-security-token). So I changed the code to the following:
private async Task<HttpRequestMessage> BuildRequestAsync(string service)
{
var request = new HttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new Uri(baseURL + service)
};
ImmutableCredentials awsCredential = await _loginService.GetAppCredentialsAsync( );
//This where I add header to the HTTP request
request.Headers.Add("x-amz-security-token", awsCredential.Token);
var signer = new AWS4RequestSigner(awsCredential.AccessKey, awsCredential.SecretKey);
request = await signer.Sign(request, "execute-api", awsRegion);
return request;
}
Related
I am trying to access AWS S3 files using Identity pool of Cognito. I am able to do all functionalities of Congito user pool and faced no issue in secret hash. I use same way here but now having the issue in this process.
I am providing my code below for more understanding. I am getting error at line AuthFlowResponse context = await user.StartWithSrpAuthAsync below.
secretHash = crypto.GetMACHash(username + "<ClientID>");
AmazonCognitoIdentityProviderConfig config = new AmazonCognitoIdentityProviderConfig();
var provider = new AmazonCognitoIdentityProviderClient("<accessKey>", "<SecretKey>",RegionEndpoint.USEast1);
CognitoUserPool userPool = new CognitoUserPool("<userpool>", "<clientId>", provider, "<clientSecret>");
CognitoUser user = new CognitoUser(username, "<clientId>", userPool, provider, secretHash);
string userpassword = "Testtest#1";
AuthFlowResponse context = await user.StartWithSrpAuthAsync(new InitiateSrpAuthRequest()
{
Password = userpassword
}).ConfigureAwait(false);
CognitoAWSCredentials s3Credentials =
user.GetCognitoAWSCredentials("<IdentityPoolID>", RegionEndpoint.USEast1);
I want to create a publisher for google cloud Pub/Sub that publish to a regional endpoint.
This is the code I am running(pretty much the code from the quickstart guide):
using Google.Cloud.PubSub.V1;
using Grpc.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using static Google.Cloud.PubSub.V1.PublisherClient;
namespace PubSub
{
class PubSubProducer
{
public async Task<int> PublishMessagesAsync(string projectId, string topicId, IEnumerable<string> messageTexts)
{
var creationSettings = new ClientCreationSettings(credentials: await GoogleGrpcCredentials.GetApplicationDefaultAsync().ConfigureAwait(false),
serviceEndpoint: "us-central1-pubsub.googleapis.com");
var customSettings = new PublisherClient.Settings
{
EnableMessageOrdering = true
};
TopicName topicName = TopicName.FromProjectTopic(projectId, topicId);
PublisherClient publisher = await PublisherClient.CreateAsync(topicName, clientCreationSettings: creationSettings, settings: customSettings);
int publishedMessageCount = 0;
var publishTasks = messageTexts.Select(async text =>
{
try
{
string ordering_key;
if (int.Parse(text) > 5)
ordering_key = "a";
else
ordering_key = "b";
string message = await publisher.PublishAsync(ordering_key, text);
Console.WriteLine($"Published message {message}");
Interlocked.Increment(ref publishedMessageCount);
}
catch (Exception exception)
{
Console.WriteLine($"An error ocurred when publishing message {text}: {exception.Message}");
}
});
await Task.WhenAll(publishTasks);
return publishedMessageCount;
}
}
}
I would like to use the ordering feature so I need the messages to be in the same region.
This code returns:
An error ocurred when publishing message 9: Status(StatusCode="Unauthenticated", Detail="Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", DebugException="Grpc.Core.Internal.CoreErrorDetailException: {"created":"#1608719160.348000000","description":"Error received from peer ipv4:<IP>","file":"T:\src\github\grpc\workspace_csharp_ext_windows_x64\src\core\lib\surface\call.cc","file_line":1062,"grpc_message":"Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.","grpc_status":16}")
When I am not suppling a regional endpoint it works fine:
var creationSettings = new ClientCreationSettings(credentials: await GoogleGrpcCredentials.GetApplicationDefaultAsync().ConfigureAwait(false));
It seems like my service account missing a permission to use this regional endpoint.
How can I fix this? What permissions am I missing?
The issue was with how I created the ClientCreationSettings.
The correct way for publisher:
var clientCreationSettings = new PublisherClient.ClientCreationSettings(serviceEndpoint: "us-central1-pubsub.googleapis.com:443");
For subscriber:
var clientCreationSettings = new SubscriberClient.ClientCreationSettings(serviceEndpoint: "us-central1-pubsub.googleapis.com:443");
My app first uses the Cognito LOGIN endpoint to obtain an Authorization Code. It then uses the TOKEN endpoint to try and obtain tokens (id_token, access_token, refresh_token) but that fails with unauthorized_client.
I do not understand why, the same client is used to access the LOGIN, and that succeeded in returning an authorization code. I'm following the documentation for the TOKEN endpoint
string clientId = ...
string clientSecret = ...
Uri redirectUri = new Uri("myapp://myhost");
string authorization_code = ... // obtained via HTTP GET on LOGIN endpoint
string accessTokenUrl = "https://<domain>.auth.<region>.amazoncognito.com/oauth2/token";
var queryValues = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "code", authorization_code },
{ "redirect_uri", redirectUri.AbsoluteUri },
{ "client_id", clientId},
};
using (HttpClient client = new HttpClient())
{
// Authorization Basic header with Base64Encoded (clientId::clientSecret)
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(
string.Format("{0}:{1}",
clientId,
clientSecret))));
// Url Encoded Content
var content = new FormUrlEncodedContent(queryValues);
// HTTPS POST
HttpResponseMessage response = await client.PostAsync(accessTokenUrl, content).ConfigureAwait(false);
string text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// test = {"error" : "unauthorized_client"}
}
The problem is two-fold:
1- System.Uri.AbsoluteUri adds a trailing / in the returned string so that my redirectUri becomes myapp://myhost/ instead of myapp://myhost
2- AWS Cognito TOKEN endpoint does not accept trailing / in a redirectURI.
The solution:
I now call redirectUri.OriginalUri instead of redirectUri.AbsoluteUri where I build the query to preserve the redirectUri as it was when I built it.
(I don't really have control over this since in my case Xamarin.Auth.OAuthAuthenticator2 calls Uri.AbsoluteUri on my behalf and transforms the redirectUri string I gave it, so I'm going to have to fix Xamarin.Auth).
i try to register a user in my amazon cognito user pool with username and password from my java backend but i always get the error:
Unable to verify secret hash for client
in the documentation i don't found any information how to pass the clientSecret in the register request and i don't like to create an (backend) app without a clientSecret.
My code looks like this
identityProvider = AWSCognitoIdentityProviderClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCreds)).withRegion(Regions.EU_CENTRAL_1).build();
Map<String, String> authParameters = new HashMap<>();
authParameters.put("USERNAME", "username");
authParameters.put("PASSWORD", "password");
authParameters.put("SECRET_HASH", "secret copy and paste from the aws console"); // i read in a forum post, that this should work
AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest();
authRequest.withAuthFlow(AuthFlowType.ADMIN_NO_SRP_AUTH);
authRequest.setAuthParameters(authParameters);
authRequest.setClientId("clientId");
authRequest.setUserPoolId("userPoolId");
AdminInitiateAuthResult authResponse = identityProvider.adminInitiateAuth(authRequest);
Thanks
Marcel
To register users you should use the SignUp API. The secret hash can be calculated as follows in Java:
public String calculateSecretHash(String userPoolclientId, String userPoolclientSecret, String userName) {
if (userPoolclientSecret == null) {
return null;
}
SecretKeySpec signingKey = new SecretKeySpec(
userPoolclientSecret.getBytes(StandardCharsets.UTF_8),
HMAC_SHA256_ALGORITHM);
try {
Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
mac.init(signingKey);
mac.update(userName.getBytes(StandardCharsets.UTF_8));
byte[] rawHmac = mac.doFinal(userPoolclientId.getBytes(StandardCharsets.UTF_8));
return Encoding.encodeBase64(rawHmac);
} catch (Exception e) {
throw new RuntimeException("Error while calculating ");
}
}
Can you please elaborate your use case of creating users from your backend instead of directly calling Amazon Cognito from your clients?
Edit: We have updated our documentation to include a section about how to compute the secret hash.
The following code works perfectly:
AdminInitiateAuthRequest adminInitiateAuthRequest = new AdminInitiateAuthRequest().withAuthFlow(AuthFlowType.ADMIN_NO_SRP_AUTH).withClientId("<ID of your client application>").withUserPoolId("<your user pool ID>")
.addAuthParametersEntry("USERNAME", "<your user>").addAuthParametersEntry("PASSWORD", "<your password for the user>");
AdminInitiateAuthResult adminInitiateAuth = identityProvider.adminInitiateAuth(adminInitiateAuthRequest);
System.out.println(adminInitiateAuth.getAuthenticationResult().getIdToken());
My goal : Have a mobile app that does not require users to ever sign in. Have these unauthenticated users hit my server.
What I have : My server is using the AWS API Gateway / AWS Lambda setup. The custom authorizer I used for AWS API Gateway was designed using this example. I Also pasted the code from this example below (A).
My Question : From the code block below (A), I get the impression I should use JWT. How can I use JWT to validate unauthenticated users when these tokens expire? If JWT is not the best thing to use, what would be?
Thanks!
(A)
var nJwt = require('njwt');
var AWS = require('aws-sdk');
var signingKey = "CiCnRmG+t+ BASE 64 ENCODED ENCRYPTED SIGNING KEY Mk=";
exports.handler = function(event, context) {
console.log('Client token: ' + event.authorizationToken);
console.log('Method ARN: ' + event.methodArn);
var kms = new AWS.KMS();
var decryptionParams = {
CiphertextBlob : new Buffer(signingKey, 'base64')
}
kms.decrypt(decryptionParams, function(err, data) {
if (err) {
console.log(err, err.stack);
context.fail("Unable to load encryption key");
} else {
key = data.Plaintext;
try {
verifiedJwt = nJwt.verify(event.authorizationToken, key);
console.log(verifiedJwt);
// parse the ARN from the incoming event
var apiOptions = {};
var tmp = event.methodArn.split(':');
var apiGatewayArnTmp = tmp[5].split('/');
var awsAccountId = tmp[4];
apiOptions.region = tmp[3];
apiOptions.restApiId = apiGatewayArnTmp[0];
apiOptions.stage = apiGatewayArnTmp[1];
policy = new AuthPolicy(verifiedJwt.body.sub, awsAccountId, apiOptions);
if (verifiedJwt.body.scope.indexOf("admins") > -1) {
policy.allowAllMethods();
} else {
policy.allowMethod(AuthPolicy.HttpVerb.GET, "*");
policy.allowMethod(AuthPolicy.HttpVerb.POST, "/users/" + verifiedJwt.body.sub);
}
context.succeed(policy.build());
} catch (ex) {
console.log(ex, ex.stack);
context.fail("Unauthorized");
}
}
});
};
My Question : From the code block below (A), I get the impression I
should use JWT. How can I use JWT to validate unauthenticated users
when these tokens expire? If JWT is not the best thing to use, what
would be?
Why do you need any validation at all? If you want to allow unauthenticated access to your API you should just do that and save yourself some effort.
Update
If you want to restrict access and not require a user to login, you may want to consider simply using Cognito Identity. It supports unauthenticated identities and migration to authenticated access either via Cognito Your User Pools or another federated provider. It is worth noting that if you go this route, you will want to secure/obscure your identity pool id properly within your app to minimize the chance of it being extracted.
Using Cognito Identity, you would be able to get AWS credentials to sign the requests to your API using standard Signature version 4 and avoid the cost and overhead of managing a custom authorizer.