I'm trying to set up a basic website (serverless on AWS) that would allow visitors to login with Google and/or Facebook. Currently I'm planning to use S3, Cognito with Federated Identities, API Gateway, Lambda (NodeJS), with DynamoDB. The client app will be using Angular.
I have the social login with Google and Facebook working, and currently I am inserting a row in a "users" table when a user logs in the first time that includes the cognitoId, name, profile picture URL, etc.
I also figure it would be a good design to store the user's information with their email address as the key, instead of something like the cognitoId so that the user can login using different Providers and see the same data. So I need to know the authenticated user's email address, but I figure it should come from Cognito and not straight from the user (since the client app shouldn't be trusted).
I believe that Cognito is storing the user's email address because I have enabled that field as required int the User Pool.
The issue I'm having is that I cannot find any information about how to get the user's email address from Cognito.
The closest that I've come is this post, but I can't find the access token anywhere: How to get user attributes (username, email, etc.) using cognito identity id
This post indicates that I may be able to use GetUser, but I again don't know where the AccessToken comes from: creating user using AWS cognito identity
If I do need to use GetUser and the AccessToken, where does it come from, and how do I generate it? Does it come from the client, or can I get it in Lambda using AWS.config.credentials?
I've been trying to figure this out for a while now and I'm feeling like I'm missing something really simple!
Firstly, go into Cognito Identity provider (in the Cognito console) and make sure your provider "Authorize Scope" is suitable. For example if you clicked on the Google provider your Authorize scope might be "profile email openid". The scope will vary by provider, but whatever scope you are using, it must provide access to the users email.
When your user logs in with an external identity provider (lets say Facebook), Cognito negotiates with Facebook and then calls your Callback URL, which is set in the 'App Client Settings' part of the Cognito console. That Callback contains a parameter called 'code' - the parameter is set in the URL of the Callback made my Cognito. The code is an OAuth token.
Now you have an OAuth token in your client you need to POST that to the AWS Token Endpoint. The token endpoint returns three new tokens in the response; a JWT ID Token, a JWT Access Token and a refresh token. Take the "id_token" attribute from the endpoint response. Parse that id_token as a json string, and take the 'email' element. Now you should have the users email address.
Here is my working example in Java. This is a servlet that gets called by the Cognito Callback.
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.nimbusds.jwt.SignedJWT;
import net.minidev.json.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
public class CognitoLandingServlet extends HttpServlet {
static final Logger LOG = LoggerFactory.getLogger(CognitoLandingServlet.class);
private static final long serialVersionUID = 1L;
public CognitoLandingServlet() {
super();
}
#Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
// Get the OpenID Connect (OAuth2) token passed back from the hosted Cognito
// Login Page
final String code = request.getParameter("code");
LOG.debug(String.format("Cognito OAuth2 code received from Cognito: %s.", code));
if (code != null) {
// do nothing, we have a code as expected
} else {
LOG.debug(String.format(
"Landing page requested without a Cognito code, the request probably didn't come from Cognito"));
// we dont have a token so redirect the user to the application sign in
// page
request.getRequestDispatcher("/signin").forward(request, response);
}
// Exchange the OIDC token for Cognito Access and ID JWT tokens using AWS
// Token
// Endpoint
// There does not appear to be a Java SDK to handle this :(
final String cognitoClientId = System.getProperty("CognitoClientId");
final String redirectUri = System.getProperty("CognitoCallBackUrl");
final String awsTokenEndpoint = System.getProperty("AwsTokenEndpoint");
final String jwt = swapOauthForJWT(cognitoClientId, code, redirectUri, awsTokenEndpoint);
// Complete the login using the JWT token string
loginWithJWT(jwt, request, response);
}
#Override
protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
}
private void loginWithJWT(final String jwtString, final HttpServletRequest request,
final HttpServletResponse response) {
final JSONParser parser = new JSONParser();
SignedJWT signedIdJWT;
try {
// Take the id token
final JSONObject json = (JSONObject) parser.parse(jwtString);
final String idToken = (String) json.get("id_token");
// Access token is not currently used
// String accessToken = (String) json.get("access_token");
// Process the id token
signedIdJWT = SignedJWT.parse(idToken);
final String userId = signedIdJWT.getJWTClaimsSet().getSubject();
// Start NEW Session and start adding attributes
final HttpSession session = request.getSession(true);
session.setAttribute("userId", userId);
final String cognitoUsername = (String) signedIdJWT.getJWTClaimsSet()
.getClaim("cognito:username");
if (cognitoUsername != null) {
user.setUserName(cognitoUsername);
session.setAttribute("username", cognitoUsername);
}
final String email = (String) signedIdJWT.getJWTClaimsSet().getClaim("email");
if (email != null) {
user.setEmail(email);
session.setAttribute("email", email);
}
// Save the user to a database (code removed for stack overflow)
//request.getRequestDispatcher("/dashboard").forward(request, response);
response.sendRedirect("/dashboard");
LOG.info(
String.format("A user with userid %s and email %s successfully signed in", userId, email));
} catch (final java.text.ParseException e) {
LOG.error(
String.format("The JWT token could not be parsed by JOSE library. %s", e.getMessage()));
} catch (final ParseException e) {
LOG.error(String.format("The JWT token could not be parsed by JSON simple library. %s",
e.getMessage()));
} catch (final IOException e) {
LOG.error(String.format("Failed to request webpage at the end of the login process - io. %s",
e.getMessage()));
}
}
private String swapOauthForJWT(final String cognitoClientId, final String oauthCode,
final String redirectUri, final String awsTokenEndpoint) throws IOException {
// Build the URL to post to the AWS Token Endpoint
final String urlParameters = String.format(
"Content-Type=application/x-www-form-urlencoded&grant_type=authorization_code&client_id=%s&code=%s&redirect_uri=%s",
cognitoClientId, oauthCode, redirectUri);
LOG.debug(String.format("User is swapping OAuth token for a JWT using URL %s", urlParameters));
final URL url = new URL(awsTokenEndpoint);
final URLConnection conn = url.openConnection();
conn.setDoOutput(true);
final OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
writer.write(urlParameters);
writer.flush();
// Read the data returned from the AWS Token Endpoint
final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
final StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = reader.readLine()) != null) {
responseStrBuilder.append(inputStr);
}
// Close the connection
writer.close();
reader.close();
LOG.debug(String.format("Finished swapping OAuth token for a JWT"));
return responseStrBuilder.toString();
}
}
You also need to add Attribute mappings in your user pool. Check if you have forgotten to add the mappings. You can find "attribute mappings" tab under "federation" inside your User Pool settings
To get the email, you have to request it to the identity provider (facebook, google, user pool).
To get the email from the user pool you have to do something like:
cognitoUser.getUserAttributes(function(err, result) {
if (err) {
alert(err);
return;
}
for (i = 0; i < result.length; i++) {
console.log('attribute ' + result[i].getName() + ' has value ' + result[i].getValue());
}
});
Cognito Identity doesn't save the emails.
Related
I'm allowing users logged in an external application to jump into our application with their access token through Keycloak's identity brokering and external to internal token exchange.
Now I'd like to establish an SSO session in an embedded JxBrowser in our application similar to a regular browser login flow, where three cookies are set in the browser: AUTH_SESSION, KEYCLOAK_SESSION(_LEGACY) and KEYCLOAK_IDENTITY(_LEGACY).
KEYCLOAK_IDENTITY contains a token of type Serialized-ID which looks somewhat similar to an ID token.
Is it possible to create the KEYCLOAK_IDENTITY cookie using the exchanged (internal) access and/or ID token and, provided that the other two cookies are correctly created as well, would this establish a valid SSO session?
Basically all I am missing is how I could obtain or create the Serialized-ID type token.
One way to achieve this:
Implement a custom endpoint following this example
Note that the provider works fine for me without registering it in standalone.xml, I'm just adding the JAR to the Keycloak Docker image.
Add a method that validates a given access token, looks up the user, gets the user session and sets the cookies in the response (most error handling omitted for brevity):
#GET
#Produces(MediaType.APPLICATION_JSON)
#Path("sso")
public Response sso(#Context final HttpRequest request) {
final HttpHeaders headers = request.getHttpHeaders();
final String authorization = headers.getHeaderString(HttpHeaders.AUTHORIZATION);
final String[] value = authorization.split(" ");
final String accessToken = value[1];
final AccessToken token = Tokens.getAccessToken(accessToken, keycloakSession);
if (token == null) {
throw new ErrorResponseException(Errors.INVALID_TOKEN, "Invalid access token", Status.UNAUTHORIZED);
}
final RealmModel realm = keycloakSession.getContext().getRealm();
final UriInfo uriInfo = keycloakSession.getContext().getUri();
final ClientConnection clientConnection = keycloakSession.getContext().getConnection();
final UserModel user = keycloakSession.users().getUserById(token.getSubject(), realm);
final UserSessionModel userSession = keycloakSession.sessions().getUserSession(realm, token.getSessionState());
AuthenticationManager.createLoginCookie(keycloakSession, realm, user, userSession, uriInfo, clientConnection);
return Response.noContent().build();
}
Disclaimer: I am not completely certain this implementation does not imply any security issues, but since Tokens.getAccessToken(accessToken, keycloakSession) does full validation of the access token, setting the cookies should only be possible with a valid access token.
For CORS, add:
#OPTIONS
#Produces(MediaType.APPLICATION_JSON)
#Path("sso")
public Response preflight(#Context final HttpRequest request) {
return Cors.add(request, Response.ok("", MediaType.APPLICATION_JSON))
.auth()
.preflight()
.allowedMethods("GET", "OPTIONS")
.build();
}
and in sso():
return Cors.add(request, Response.ok("", MediaType.APPLICATION_JSON))
.auth()
.allowedMethods("GET")
.allowedOrigins(token)
.build();
What I am uncertain about is why Firefox preflights the GET request, making it necessary to handle that.
I want to authenticate AAD users to access powerBi resources through MSAL by using application ID and secret. So i want to get the access token and cache it in SQL Db.
went through the documentation but it explains the scenario of using MSAL for sign-in.
also went through the tutorial
i was able to to do the necessary implementations to get the token.
how can i get the access token and cache it, in a scenario like this?
As indicated in other answers, caching tokens are useful in case when you have users signing in, as once the access token expires (typically after 1 hour), you don't want to keep prompting the users to re-authenticate.
So help with these scenarios, Azure AD issues a refresh token along with an access token that is used to fetch access tokens once they expire. Caching is required to cache these refresh tokens as they are valid for 90 days.
When an app signs as itself (and not signing in a user), the client credentials flow is used and it only needs the app id (clientId) and the credential (secret/certificate) to issue an access token. The MSAL library will automatically detect when the access token expires and will use the clientId/credential combination to automatically get a new access token. So caching is not necessary.
The sample you should be looking at is this one.
I'n not sure to understand, I hope these few lines of code will help you.
First, customize token cache serialization :
public class ClientApplicationBuilder
{
public static IConfidentialClientApplication Build()
{
IConfidentialClientApplication clientApplication =
ConfidentialClientApplicationBuilder
.Create(ClientId)
.WithRedirectUri(RedirectUri)
.WithClientSecret(ClientSecret)
.Build();
clientApplication.UserTokenCache.SetBeforeAccessAsync(BeforeAccessNotification);
clientApplication.UserTokenCache.SetAfterAccessAsync(AfterAccessNotification);
return clientApplication;
}
private static async Task<byte[]> GetMsalV3StateAsync()
{
//TODO: Implement code to retrieve MsalV3 state from DB
}
private static async Task StoreMsalV3StateAsync(byte[] msalV3State)
{
//TODO: Implement code to persist MsalV3 state to DB
}
private static async Task BeforeAccessNotification(TokenCacheNotificationArgs args)
{
byte[] msalV3State = await GetMsalV3StateAsync();
args.TokenCache.DeserializeMsalV3(msalV3State);
}
private static async Task AfterAccessNotification(TokenCacheNotificationArgs args)
{
if (args.HasStateChanged)
{
byte[] msalV3State = args.TokenCache.SerializeMsalV3();
await StoreMsalV3StateAsync(msalV3State);
}
}
}
Here's an example to acquire token (by Authorization Code) :
public class MsAccountController
: Controller
{
private readonly IConfidentialClientApplication _clientApplication;
public MsAccountController()
{
_clientApplication = ClientApplicationBuilder.Build();
}
[HttpGet]
public async Task<IActionResult> Index()
{
Uri authorizationRequestUrl = await _clientApplication.GetAuthorizationRequestUrl(ClientApplicationHelper.Scopes).ExecuteAsync();
string authorizationRequestUrlStr = authorizationRequestUrl.ToString();
return Redirect(authorizationRequestUrlStr);
}
[HttpGet]
public async Task<IActionResult> OAuth2Callback(string code, string state)
{
AuthenticationResult authenticationResult = await _clientApplication.AcquireTokenByAuthorizationCode(scopes, code).ExecuteAsync();
return Ok(authenticationResult);
}
}
Finally, acquire a token silently and use auth result for your API client :
public class TaskController
: Controller
{
private readonly IConfidentialClientApplication _clientApplication;
public TaskController()
{
_clientApplication = ClientApplicationBuilder.Build();
}
[HttpGet]
public async Task<IActionResult> Index()
{
IEnumerable<IAccount> accounts = await _clientApplication.GetAccountsAsync();
AuthenticationResult result = await _clientApplication.AcquireTokenSilent(ClientApplicationHelper.Scopes, accounts.FirstOrDefault()).ExecuteAsync();
//TODO: Create your API client using authentication result
}
}
Regards
You can cache the access token (actually, the library does this already), but it is valid for 1 hour only. So it makes no sense to save it in a database, because it will expire quickly.
You should cache the credentials needed to obtain the token (user name and password, app ID and secret, or certificate) and obtain a token when needed.
I've done this for a confidential client application, where I connected to O365 in order to send email.
First, register your app in azure app as the docs say.
Then, set up your confidential client application and use as singleton.
var app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithClientSecret(clientSecret)
.WithRedirectUri(redirectUri)
.WithLegacyCacheCompatibility(false)
.WithAuthority(AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount)
.Build();
app.AddDistributedTokenCache(services => {
services.AddDistributedTokenCaches();
services.AddDistributedSqlServerCache(options => {
options.SchemaName = "dbo";
options.TableName = "O365TokenCache";
options.ConnectionString = sqlCacheConnectionString;
options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
});
});
services.AddSingleton<IConfidentialClientApplication>(app);
The first time you connect a user, you need to redirect to Microsoft identity. You can create the URL using:
var authUrl = await app.GetAuthorizationRequestUrl(new[] { "email", "offline_access", "https://outlook.office.com/SMTP.Send" }).ExecuteAsync();
(Check your scopes are what you want)
When they come back to your redirect url you then get the code from query string and acquire the refresh token:
var token = await app.AcquireTokenByAuthorizationCode(scopes, code).ExecuteAsync();
When you do this, MSAL will cache the access token and refresh token for you, but here's the thing they don't mention: you have to create the table in SQL yourself! If you don't, it just silently fails.
dotnet tool install -g dotnet-sql-cache
dotnet sql-cache create "<connection string>" dbo O365TokenCache
Once you have the access token the first time you can use the following later
var account = await app.GetAccountAsync(accountId);
var token = await app.AcquireTokenSilent(scopes, account).ExecuteAsync();
When you get the access token the first time, you need to look at token.Account.HomeAccountId.Identifier as this is the ID that you need when you call GetAccountAsync. For some reason, GetAccountsAsync (note the extra "s") always returns empty for me but passing the correct ID to GetAccountAsync does return the right one.
For me, I simply store that ID against the logged in user so that I can get that ID at a later time.
I have created a user pool in cognito and after login to my application, I store the three tokens generated from AWS Cognito in session.
I need to pass these tokens to a third party API and they will in return send me the response along with the token that was sent.
How do I validate token with just user pool ID and client App Id.
This AWS Blog post explains the solution in detail.
The ID Token and Access Token generated by Amazon Cognito are JWTs. Cognito uses two RSA key pairs to generate these tokens. The private key of each pair is used to sign the tokens. The public keys can be used to verify the tokens. These public keys are available at
https://cognito-idp.{REGION}.amazonaws.com/{YOUR_USER_POOL_ID}/.well-known/jwks.json
Using the Key ID from this path, you need to get the public key. Using this public key, you can verify the tokens.
Following is a NodeJS code snippet to implement the above logic. Complete example can be seen at this commit
const jwt = require('jsonwebtoken'); // JS Lib used to verify JWTs
const jwksClient = require('jwks-rsa'); // JS Lib to get keys from a URL
const USER_POOL_ID = "<YOUR_USER_POOL_ID>";
const CLIENT_ID = "<YOUR_CLIENT_ID>";
const REGION = "<YOUR_REGION>";
const ISSUER_URI = "https://cognito-idp." + REGION + ".amazonaws.com/" + USER_POOL_ID;
const JWKS_URI = ISSUER_URI + "/.well-known/jwks.json";
// Generate a client to read keys from the Cognito public URL
let client = jwksClient({
jwksUri: JWKS_URI,
});
// Async function to get public keys from key Id in jwks.json
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
var signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
// Verify jwt. getKey function will take the header from your idToken and get
the corresponding public key. This public key will be used by jwt.verify() to
actually verify the token.
jwt.verify(idToken, getKey, { audience: CLIENT_ID, issuer: ISSUER_URI }, function(err, decoded) {
console.log("RES", err, decoded);
// Additional verifications like token expiry can be done here.
}
I am trying to authenticate my user in a Xamarin Forms cross platform app using AWS Cognito User Pools.
I am able to sign up a user using the SignUpAysnc() and I can see it populate in the user pool in the AWS console.
CognitoUserPool userPool = AmazonUtils.UserPool;
Dictionary<string, string> userAttributes = new Dictionary<string, string>();
userAttributes.Add("email", email);
userAttributes.Add("given_name", given_name);
userAttributes.Add("family_name", family_name);
userAttributes.Add("gender", gender);
userAttributes.Add("birthdate", birthdate);
userAttributes.Add("address", address);
userAttributes.Add("locale", locale);
userAttributes.Add("phone_number", phone_number);
await userPool.SignUpAsync(email, Password, userAttributes, null);
However when I try to use the email and password provided to sign in I keep getting this exception:
[0:] Missing Authentication Token
My current authentication code is:
private async void LoginButton_Clicked(object sender, EventArgs e)
{
AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest();
authRequest.ClientId = Constants.ClientID;
authRequest.AuthParameters.Add("email", "test.user#email.com");
authRequest.AuthParameters.Add("password", "Password12!");
authRequest.UserPoolId = Constants.AuthPoolID;
authRequest.AuthFlow = AuthFlowType.ADMIN_NO_SRP_AUTH;
try
{
AdminInitiateAuthResponse response = await AmazonUtils.IdentityClientProvider.AdminInitiateAuthAsync(authRequest);
}
catch(Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
}
}
Does anyone know what I might be missing?
The thing is that you need to initiate the authentication challenge first, and then pass password as well as encrypted string, that contains pool name, username, secret block and other information.
This is a good .Net example: http://blog.mmlac.com/aws-cognito-srp-login-c-sharp-dot-net/
Note that you need to add Org.BouncyCastle.Math and Org.BouncyCastle.Security via NuGet to your project in order to compile it.
There is another example: https://gist.github.com/dbeattie71/44ea3a13145f185d303e620c299ab1c5
It looks promising, but I did not check it so I can't guaranty that it works. From it you could get an understanding on how overall process looks like.
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());