What I am trying to do: Authenticate my users using ADFS, pass the SAML response token to AWS and get back credentials which I can then use to access AWS resources.
What I am able to do now: Sign in successfully through ADFS and get the SAML token back which confirms the successfully sign in.
What is not working: Calling the AWS.STS.assumeRoleWithSaml functions gets a 403 Access Denied error
How it works thus far:
Users click a button on my application, which calls the following:
var RPID = encodeURIComponent('urn:amazon:webservices');
var result = 'https://virtualMachine.eastus.cloudapp.azure.com/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=' + RPID;
window.location.href = result;
A successful sign in here returns back to the application a SAML response token
var saml = new URL(window.location.href);
var token = saml.searchParams.get('SAMLResponse');
The application then calls assumeRoleWithSAML to get back credentials. The Principal ARN refers to the identity provider I am trying to access and the RoleARN refers to a role which has full access to everything:
authenticateSAMLwithCognito(token) {
//define our security token service object
var sts = new AWS.STS();
//build our parameter object
var params = {
//cognito identity provider
PrincipalArn: 'arn:aws:iam::accountid:saml-provider/wmpo-adfs',
//role assuming
RoleArn: 'arn:aws:iam::accountid:role/ADFS-Dev',
//authorization
SAMLAssertion: token
}
console.log("Parameters sent", params);
sts.assumeRoleWithSAML(params, (err, data) => {
if(err) console.log(err);
else console.log("Success!", data);
})
}
However the response from this exchange is:
I am really unsure why this is, but if anyone has some helpful pushes that would be great! Thanks and happy new year
Wow that only took days, but though I was constantly stumbling, I finally made it to the point I was trying to get.
The answer was in the same credentials get function that I used when I authenticated users through a username password combo by way of a Cognito User Pool.
authenticateThroughCognito(token) {
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-west-2:IdentityPoolId',
Logins: {
'arn:aws:iam::accountId:saml-provider/wmpo-adfs' : token
}
});
(AWS.config.credentials as AWS.Credentials).get((err) => {
if(err) console.log(err);
else {
console.log("Success");
console.log(AWS.config.credentials);
}
})
}
Related
I have an Angular 10 site. I have an AWS Lambda (ASP.NET Core 3.1) that does authentication with AWS Cognito for users with email/password. But I want to allow users to also use Facebook (and eventually Google) to log in as well. In the Facebook/Google scenario my thought is not to allow access to AWS services directly for now (like S3, etc) but to interact with with my other lambda via a bearer token. I have a Cognito User Pool for which I created a Facebook identity provider and mappings. I read somewhere that I'd need an Identity Pool. So I created that and put in my Cognito user pool as a provider as well as Facebook.
Using the JavaScript code:
loginWithFacebook = () => {
const login$ = from(this.facebookService.login());
login$.subscribe(
(response: LoginResponse) => {
console.log(response);
this.facebookLoginToAWS(response);
},
error => {
console.error(error);
}
);
};
I can get a Facebook auth response no problem. Then using this code that you see on every blog, Stack Overflow post, and even in AWS's documentation (of course, substituting my own IdenityPoolId):
private facebookLoginToAWS = (facebookResponse: LoginResponse) => {
console.log('facebookLoginToAWS', facebookResponse);
if (facebookResponse.status === 'connected' && facebookResponse.authResponse) {
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
Logins: { 'graph.facebook.com': facebookResponse.authResponse.accessToken }
}, {
region: 'eu-west-1'
});
AWS.config.credentials.get((err) => {
if (err) {
return console.log("Error", err);
}
console.log("Cognito credentials", AWS.config.credentials);
console.log("Cognito Identity Id", AWS.config.credentials.identityId);
});
} else if (facebookResponse.status === 'not_authorized') {
document.getElementById('facebookStatus').innerHTML = 'Please log into this app.';
} else {
document.getElementById('facebookStatus').innerHTML = 'Please log into Facebook.';
}
};
I can get back a session token (in addition to a ton of other stuff like accesKeyId, identityId, and secretAccessKey).
But what do I do with that session token? Perhaps I'm confused, but I would think because there is a mapping between Facebook fields and Cognito fields, that somehow that Facebook user would be migrated into the Cognito user pool and I could get a JWT token for that user for my other lambdas. But after checking the AWS dashboard, I can see a log in (I think) in the Identity Pool but there is no corresponding User Pool entry.
Do I somehow have to manually migrate it over (using the AWS JavaScript SDK)? I don't want to use the Amplify library. Am I thinking about it wrong? Do I somehow use the session token as a bearer token with my other lambdas? Do I need to add that person as a user pool user somehow?
I am using javascript sdk for AWS cognito and able to login with aws cognito and receiving tokens in response.
I can see that the user session is valid until I refresh the page. Please suggest how the user session can persist after refreshing the page.
Below is my code.
function getSession() {
let poolData = {
UserPoolId: _config.cognito.userPoolId, // Your user pool id here
ClientId: _config.cognito.clientId, // Your client id here
};
//alert(sessionStorage.getItem("SessionName"));
let userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
cognitoUser = userPool.getCurrentUser();
cognitoUser.getSession(function (err, session) {
if (err) {
alert(err);
return;
}
console.log('session validity: ' + session.isValid());
//Set the profile info
cognitoUser.getUserAttributes(function (err, result) {
if (err) {
console.log(err);
return;
}
console.log("------>>" + result);
//document.getElementById("email_value").innerHTML = result[2].getValue();
});
});
}
good news - the SDK does this for you. Check out their code for the getsession method
You can see they store the tokens to local storage for you.
To view the tokens from Google Chrome, go to developer tools -> Application. You should see a 'Storage' section on the left hand side. Open Local Storage, the tokens are saved under the URL of the application.
You should not need to access these token directly, the SDK will fetch and save the tokens as required when you call different methods.
I want to authenticate users using Cognito, with option to use Facebook. User can sign_in/sign_up using either of those options.
I have created Cognito User Pool and Cognito Federated Identity, and also I have created Facebook App for authentication. Both User Pool and Facebook app are connected to Federated identity.
When I sign_up and later authenticate Cognito User via Cognito User Pool, then Cognito returns accessToken, which I store in localStorage on front and use whenever needed for athentication.
I have /authenticate endpoint (express), that takes in username & password, and returns accessToken if all went well. Whenever I make API call that requires auth, I send accessToken that I have in local storage. It goes, more or less as this:
// POST user/authenticate
const authenticationData = {
Username: username,
Password: password
}
authenticationDetails = new AuthenticationDetails(authenticationData)
const userData = {
Username: username,
Pool: userPool()
}
cognitoUser = new CognitoUser(userData)
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (res) => resolve(res), // here I get accessToken
onFailure: (err) => {
console.log('[authenticateUser error]', err)
reject(err)
},
//...
However
When I use Facebook, I do not get accessToken I could use in same fashion. I get accessToken from Facebook via FB.login, I pass it to Cognito to authenticate, and then I don't know what to do, because I cannot get any token that could be used to authenticate API calls, that require Cognito Authentication.
Here's what I do:
await window.FB.login((response) => {
props.userFacebookSignIn(response)
})
// ...
call(foo, 'users/facebook_sign_in', { accessToken: payload.facebookAccessToken })
// ...
// users/facebook_sign_in
AWS.config.region = config.AWSRegion
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'foo',
Logins: {
'graph.facebook.com': facebookAccessToken
}
})
AWS.config.credentials.get((err) => {
// Here I get no errors, I presume that I have logged Facebook user in
const accessKeyId = AWS.config.credentials.accessKeyId
const secretAccessKey = AWS.config.credentials.secretAccessKey
const sessionToken = AWS.config.credentials.sessionToken
// here I can do stuff probably,
// but I would like to receive token that would allow me to do stuff,
// rather than context I can do stuff in
})
While I am doing all of this, I have this feeling, that devs at AWS implemented Cognito as frontend solution, rather than something to be used in backend. Correct me if I am wrong.
Nevertheless, I would like to be able authenticate api calls using Cognito and Facebook interchangeably in express middleware.
Is that possible? Thanks.
I have used federated identity for salesforce single sign on but i imagine the steps will the same. After authenticating with facebook you will recieve and id_token from them in response. You have to pass this as a parameter in the getId method:
var params = {
IdentityPoolId: 'STRING_VALUE', /* required */
AccountId: 'STRING_VALUE',
Logins: {
'<IdentityProviderName>': 'STRING_VALUE',
/* 'graph.facebook.com': ... */
}
};
cognitoidentity.getId(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
In the result you will get an identity id which you can save somewhere so that you don't have to make this call everytime while authenticating. Now take this identity id and make the getCredentialsForIdentity call:
response = client.get_credentials_for_identity(
IdentityId='string',
Logins={
'string': 'string'
},
CustomRoleArn='string'
)
This will finally give you the temporary access key, secret key and session key you need.
I decided to use oAuth.
Here's quick & dirty look on how it's done
In AWS Cognito
1) Set up Cognito User Pool. Add App Client save App client id & App client secret as COGNITO_CLIENT_ID and COGNITO_CLIENT_SECRET
2) Go to Federation > Identity providers and add your Facebook app ID and App secret (both you will find in Facebook app panel)
3) Go to App integration > App client settings click "Select all", set up your Callback URL, mine is localhost:5000/facebook also select Authorization code grant and Allowed OAuth Scopes (save scopes to say: COGNITO_SCOPES)
4) Now go to App integration > Domain name and enter your custom domain; let's say example-app-debug so it's: https://example-app-debug.auth.us-east-1.amazoncognito.com
That's all there is to Cognito
no the Facebook part
5) Settings > Basic add example-app-debug.auth.us-east-1.amazoncognito.com to your App domains - Save Changes
6) In Facebook Login > Settings in Valid OAuth Redirect URIs add this URL: https://example-app-debug.auth.us-east-1.amazoncognito.com/oauth2/idpresponse and Save Changes
and the code
In browser, redirect user to this url when Login w. Facebook button is clicked:
window.location.href =
`https://example-app-debug.auth.us-east-1.amazoncognito.com/oauth2/authorize` +
`?identity_provider=Facebook` +
`&redirect_uri=http://localhost:5000/facebook` +
`&response_type=code` +
`&client_id=${COGNITO_CLIENT_ID}` +
`&scope=${COGNITO_SCOPES}`
this call should come back to you with a code, like this: http://localhost:5000/facebook?code=foo-bar-code Send this code to your backend.
In backend, do this:
const axios = require('axios')
const url = `` +
`https://${COGNITO_CLIENT_ID}:${COGNITO_CLIENT_SECRET}` +
`#example-app-debug.auth.us-east-1.amazoncognito.com/oauth2/token` +
`?grant_type=authorization_code` +
`&code=foo-bar-code` + // <- code that came from Facebook
`&redirect_uri=http://localhost:5000/facebook` +
`&client_id=${COGNITO_CLIENT_ID}`
const response = await axios.post(url)
// response should have access_token, refresh_token and id_token in data
You send access_token, refresh_token and id_token back to frontend and save them in local storage and use them to authenticate and Done.
I'm creating a custom token on backend server while login and below is the code:
UserSchema.methods.generateAuthToken = function() {
var user = this;
var access = "auth";
return firebaseAdmin.default
.auth()
.createCustomToken(user._id.toHexString())
.then(function(token) {
user.tokens = user.tokens.concat([{ access, token }]);
return user.save().then(() => {
return token;
});
});
};
and storing this token inside mongodb and sending it back to client via header named "x-auth" and then stored it inside cookies.
On client side, using this token to signin like below:
axios({
url: "/api/users/login",
method: "POST",
data: {
email: data.email,
password: data.password
}
}).then(res => {
Cookies.set("x-auth", res.headers["x-auth"], { expires: 7 });
return firebase
.auth()
.signInWithCustomToken(res.headers["x-auth"])
.then(response => {
console.log("CA", response);
response
.getIdToken()
.then(idToken => {
Cookies.set("idToken", idToken, { expires: 7 });
})
.catch(e => {});
dispatch(
login({
token: Cookies.get("x-auth")
})
);
});
});
Now in order to call an api to fetch all users, I'm sending these tokens, custom token and idToken back to server:
const authenticate = (req, res, next) => {
const token = req.header("x-auth");
const idToken = req.header("idToken");
User.findByToken(token, idToken)
.then(user => {
if (!user) {
return Promise.reject({
message: "no user found with the associated token!"
});
}
req.user = user;
req.token = token;
next();
})
.catch(e => {
res.status(401).send(setErrorMessage(e.message));
});
};
And findByToken is as below:
UserSchema.statics.findByToken = function(token, idToken) {
var User = this;
return firebaseAdmin.default
.auth()
.verifyIdToken(idToken)
.then(function(decodedToken) {
var uid = decodedToken.uid;
return User.findOne({
_id: uid,
"tokens.access": "auth",
"tokens.token": token
});
})
.catch(function(e) {
return Promise.reject(e);
});
};
Why do I have to send both these tokens for authorization, is there anything wrong with the concept here.
https://firebase.google.com/docs/auth/admin/verify-id-tokens
Warning: The ID token verification methods included in the Firebase Admin SDKs are meant to verify ID tokens that come from the client SDKs, not the custom tokens that you create with the Admin SDKs. See Auth tokens for more information.
Please clarify if I can verify the custom token instead of idToken to retrieve userId and match it up with DB, instead of using different tokens for one purpose or I'm doing something wrong here and custom token should not be stored inside DB, and there is some other approach to it.
And now after sometime when I try to fetch all users, it says:
Firebase ID token has expired. Get a fresh token from your client app and try again (auth/id-token-expired). See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
When working with tokens, you need to differentiate each token and what it is used for, also each token has related properties.
Custom Tokens: These tokens will be generated in the server (as you are doing) and are used in the client side to authenticate the client. These tokens expire after one hour.
Session ID Token: (I'm calling it "Session ID Token" just to differentiate it) When the client autheticates with any of the authentication providers, the SDK will exchange the information for an ID token used for the session. The same applies for custom tokens, where the SDK exchanges the custom token for an ID Token. ID Tokens are also short live and will expire after one hour. When getting the ID Token, the SDK also receives a refresh Token which is used for refreshing the session ID Token. The ID token is used when doing authenticated requests to Firebase.
ID Token for verification: To get this token, you will need to call the function "getIDToken" (for Web). This function will return an ID Token that can be used only to verify requests coming from the client to your server. Similar to the other tokens, this one expires after one hour. When calling the getIDToken, the function will request a new one if the current is expired.
To verify the ID Token, you must use the mentioned token. If you try to use the session ID Token or the Custom token, you will get errors. The same applies if you try to use an expired ID Token.
All the calls and tasks to refresh the ID Tokens are handled by the SDKs behind the scenes.
It is not a good practice to store these tokens as they will expire after one hour, is better if you call the appropriate functions to get the latets tokens and pass the correct ID Token to be verified.
Lastly, you can find in these links the properties used for generating the custom token and the ID Token for verification.
I am trying to get OAuth to work on node.js. I found this in the documentation of node-oauth:
var OAuth= require('oauth').OAuth;
var oa = new OAuth(requestUrl,accessUrl,consumerKey,consumerSecret,"1.0A",responseUrl,"HMAC-SHA1");
The next step in the official tutorial says:
"Then get hold of a valid access token + access token secret as per the normal channels"
What are these "normal channels"?
I know that the user has to authenticate somehow on the "vendor" site and that by some way a response url is called, but I can't find a description how to implement this. Can someone enlighten me?
I'm not sure what OAuth service you are trying to connect to so I'll just use twitter as an example. After you create your OAuth object you need to first request an oauth token. When you get that token, then you need to redirect to, for twitter, their authenticate page which either prompts them to login, then asks if it's ok for the app to login.
oa.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results){
if (error) new Error(error.data)
else {
req.session.oauth.token = oauth_token
req.session.oauth.token_secret = oauth_token_secret
res.redirect('https://twitter.com/oauth/authenticate?oauth_token='+oauth_token)
}
});
When you first created the OAuth object, you set a responseURL, or the callback url. It can be anything, for my app its just /oauth/callback. In that callback you receive the oauth verifier token. You then use both the oauth request token and oauth verifier token to request the access tokens. When you receive the access tokens you will also receive anything else they pass, like their username.
app.get('/oauth/callback', function(req, res, next){
if (req.session.oauth) {
req.session.oauth.verifier = req.query.oauth_verifier
var oauth = req.session.oauth
oa.getOAuthAccessToken(oauth.token,oauth.token_secret,oauth.verifier,
function(error, oauth_access_token, oauth_access_token_secret, results){
if (error) new Error(error)
console.log(results.screen_name)
}
);
} else
next(new Error('No OAuth information stored in the session. How did you get here?'))
});
Hope this helps! I had the same problems when I started on this.
The access token is issued to your application after walking the user through the "OAuth dance" (as its affectionately known). This means obtaining a request token and redirecting the user to the provider (Twitter, in this case) for authorization. If the user grants authorization, Twitter redirects the user back to your application with a code that can be exchanged for an access token.
node-oauth can be used to manage this process, but a higher-level library will make it much easier. Passport (which I'm the author of), is one such library. In this case, check out the guide to Twitter authentication, which simplifies the OAuth dance down to a few lines of code.
After that, you can save the access token in your database, and use it to access protected resources in the usual manner using node-oauth.
An update to post tweet to user timeline:
#mattmcmanus, Extending #mattmcmanus nice answer, I would like to post a tweet to timeline. For this, I am using the same code as mattcmanus given above.
Step 1:
oa.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results){
if (error) new Error(error.data)
else {
req.session.oauth.token = oauth_token
req.session.oauth.token_secret = oauth_token_secret
res.redirect('https://twitter.com/oauth/authenticate?oauth_token='+oauth_token)
}
});
Step 2:
app.get('/oauth/callback', function(req, res, next){
if (req.session.oauth) {
req.session.oauth.verifier = req.query.oauth_verifier
var oauth = req.session.oauth
oa.getOAuthAccessToken(oauth.token,oauth.token_secret,oauth.verifier,
function(error, oauth_access_token, oauth_access_token_secret, results){
if (error) new Error(error){
console.log(results.screen_name)
}else{
// NEW CODE TO POST TWEET TO TWITTER
oa.post(
"https://api.twitter.com/1.1/statuses/update.json",
oauth_access_token, oauth_access_token_secret,
{"status":"Need somebody to love me! I love OSIpage, http://www.osipage.com"},
function(error, data) {
if(error) console.log(error)
else console.log(data)
}
);
// POST TWEET CODE ENDS HERE
}
}
);
} else
next(new Error('No OAuth information stored in the session. How did you get here?'))
});
I have added oauth_access_token & oauth_access_token_secret in commented code. This will post a tweet update to user's timeline. Happy tweeting!!!