Configuring Custom Challenge to do token swap in AWS Cognito - aws-lambda

How do you properly configure custom challenge by sending an access_token from
cookie without a session?
const {
CognitoUserPool,
AuthenticationDetails,
CognitoUser
} = require('amazon-cognito-identity-js');
async function asyncAuthenticateUser(cognitoUser, cognitoAuthenticationDetails) {
return new Promise(function (resolve, reject) {
cognitoUser.initiateAuth(cognitoAuthenticationDetails, {
onSuccess: resolve,
onFailure: reject,
customChallenge: resolve
})
})
}
async function asyncCustomChallengeAnswer(cognitoUser, challengeResponse) {
return new Promise(function (resolve, reject) {
cognitoUser.sendCustomChallengeAnswer(challengeResponse, {
onSuccess: resolve,
onFailure: reject,
customChallenge: reject // We do not expect a second challenge
}
})
}
// omitted part of codes for brevity...
// We have tokens as cookie already that means a successful login previously succeeded
// but this login has probably been done from a different client with a different client_id
// We call the custom auth flow along with the token we have to get a new one for the current client_id
// For this to work we need to extract the username from the cookie
let tokenDecoded = jwt_decode(cookies.access_token);
let tokenUsername = tokenDecoded['username'];
var authenticationData = {
Username: tokenUsername,
AuthParameters: {
Username: tokenUsername,
}
};
var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
var poolData = {
UserPoolId: process.env.AUTH_AMPLIFYIDENTITYBROKERAUTH_USERPOOLID,
ClientId: client_id
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var userData = {
Username: tokenUsername,
Pool: userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.setAuthenticationFlowType("CUSTOM_AUTH");
try {
// Initiate the custom flow
await asyncAuthenticateUser(cognitoUser, authenticationDetails);
// Answer the custom challenge by providing the token
var result = await asyncCustomChallengeAnswer(cognitoUser, cookies.access_token);
var encrypted_id_token = await encryptToken(result.getIdToken().getJwtToken());
var encrypted_access_token = await encryptToken(result.getAccessToken().getJwtToken());
var encrypted_refresh_token = await encryptToken(result.getRefreshToken().getToken());
params.Item.id_token = encrypted_id_token;
params.Item.access_token = encrypted_access_token;
params.Item.refresh_token = encrypted_refresh_token;
}
catch (error) {
console.log("Token swap fail, this may be a tentative of token stealing");
return { // Redirect to login page with forced pre-logout
statusCode: 302,
headers: {
Location: '/?client_id=' + client_id + '&redirect_uri=' + redirect_uri + '&authorization_code=' + authorizationCode + '&forceAuth=true' + insertStateIfAny(event),
}
};
}
I have this part of codes where it invoke the iniateAuth then send a custom challenge answer.
// Initiate the custom flow
await asyncAuthenticateUser(cognitoUser, authenticationDetails);
// Answer the custom challenge by providing the token
var result = await asyncCustomChallengeAnswer(cognitoUser, cookies.access_token);
This complains due to the request session being empty.
The below are the auth challenge for create.
function handler(event, context, callback) {
// This function does nothing, the challenge do not need to be prepared
// Verify challenge will just verify the token provided
event.response.publicChallengeParameters = {};
event.response.publicChallengeParameters.question = 'JustGimmeTheToken';
event.response.privateChallengeParameters = {};
event.response.privateChallengeParameters.answer = 'unknown';
event.response.challengeMetadata = 'TOKEN_CHALLENGE';
event.response.challengeResult = true;
callback(null, event);
}
This is for the define challenge.
function handler(event, context, callback) {
// This function define the sequence to obtain a valid token from a valid token of another client
if (event.request.session.length == 0) {
// This is the first invocation, we ask for a custom challenge (providing a valid token)
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
} else if (
event.request.session.length == 1 &&
event.request.session[0].challengeName == 'CUSTOM_CHALLENGE' &&
event.request.session[0].challengeResult == true
) {
// The custom challenge has been answered we retrun the token
event.response.issueTokens = true;
event.response.failAuthentication = false;
}
context.done(null, event);
}
It goes both in this auth challenge but never goes to the verify challenge where
the is the below
function handler(event, context, callback) {
const params = {
AccessToken: event.request.challengeAnswer
};
const userInfo = await cognito.getUser(params).promise();
if (userInfo.Username === event.username) {
event.response.answerCorrect = true;
} else {
// Someone tried to get a token of someone else
event.response.answerCorrect = false;
}
callback(null, event);
}

Related

AdonisJS Socket.IO JWT authentication

How can you define the auth provider? Now every time the auth variable is undefined in the playerLogin method.
I'm are using Adonis v4.1
The code:
start/socket.js
const Server = use('Server')
const io = use('socket.io')(Server.getInstance())
// Define controllers here
// Example: const WSController = use('App/Controllers/Http/ChatController')
const AuthenticateController = use('App/Controllers/Http/AuthenticateController');
io.on('connection', function (socket) {
// Define here the controller methods
// Example: WSController.goMessage(socket, io)
AuthenticateController.playerLogin(socket, io);
AuthenticateController.playerRegister(socket, io);
})
AuthenticateController.js
const Hash = use('Hash')
const User = use('App/Models/User')
class AuthenticateController {
static playerLogin(socket, io, {auth}) {
socket.on('playerLogin', async (data) => {
console.log('WORKS')
if (await auth.attempt(data.email, data.password)) {
let user = await User.findBy('email', data.email)
let accessToken = await auth.generate(user)
socket.emit('sendPlayerToken', { token: accessToken });
} else {
socket.emit('sendPlayerToken', { token: 'Credentials are incorrect' });
}
});
}
static playerRegister(socket, io) {
socket.on('playerRegister', async (data) => {
const safePassword = await Hash.make(data.password)
const user = new User()
user.username = data.username
user.email = data.email
user.password = safePassword
await user.save()
socket.emit('sendPlayerRegister', { success: true });
});
}
}
Kind regards,
Corné
As this discussion bellow:
https://github.com/adonisjs/core/discussions/2051?sort=new#discussioncomment-274111
You can use the API guard and authenticate using the code from the answer of M4gie.
You only need to install the crypto package with yarn add crypto and if you don't want to create the object to display the socket errors, just turn it into a string: throw new Error('SocketErrors:MissingParameter').

IdentityServer4 Access token updating

Last week I am trying to configure the IdentityServer4 to get an access token automatically updating.
I had an API:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5100";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
My MVC client configuration:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5100";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
});
And the IdentityServer's clients configuration:
return new List<Client>
{
new Client
{
ClientId = "mvc",
ClientName = "My mvc",
AllowedGrantTypes = GrantTypes.Hybrid,
RequireConsent = false,
AccessTokenLifetime = 10,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5102/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5102/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api1"
},
AllowOfflineAccess = true
}
};
On the client side I use AJAX queries to call the API to get/post/put/delete data. I add the access token to the request and get the result.
private async getAuthenticationHeader(): Promise<any> {
return axios.get('/token').then((response: any) => {
return { headers: { Authorization: `Bearer ${response.data}` } };
});
}
async getAsync<T>(url: string): Promise<T> {
return this.httpClient
.get(url, await this.getAuthenticationHeader())
.then((response: any) => response.data as T)
.catch((err: Error) => {
console.error(err);
throw err;
});
}
The access token is provided by the MVC client method:
[HttpGet("token")]
public async Task<string> GetAccessTokenAsync()
{
return await HttpContext.GetTokenAsync("access_token");
}
It works fine. After access token expired I get 401 on the client side, so it would be great to have an opportunity to update access token automatically when it was expired.
According to a documentation I supposed, that It can be reached by setting AllowOfflineAccess to true and adding suitable scope "offline_access".
Maybe I don't understand the right flow of the access and refresh tokens usages. Can I do it automatically or it is impossible? I suppose, that we can use refresh tokens in out queries, but I don't understand how.
I've read a lot of SO answers and github issues but I am still confused. Could you help me to figure out?
After investigation and communicating in comments I've found the answer. Before every API call I get the expite time and according to the result update access_token or return existing:
[HttpGet("config/accesstoken")]
public async Task<string> GetOrUpdateAccessTokenAsync()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var expiredDate = DateTime.Parse(await HttpContext.GetTokenAsync("expires_at"), null, DateTimeStyles.RoundtripKind);
if (!((expiredDate - DateTime.Now).TotalMinutes < 1))
{
return accessToken;
}
lock (LockObject)
{
if (_expiredAt.HasValue && !((_expiredAt.Value - DateTime.Now).TotalMinutes < 1))
{
return accessToken;
}
var response = DiscoveryClient.GetAsync(_identitySettings.Authority).Result;
if (response.IsError)
{
throw new Exception(response.Error);
}
var tokenClient = new TokenClient(response.TokenEndpoint, _identitySettings.Id, _identitySettings.Secret);
var refreshToken = HttpContext.GetTokenAsync("refresh_token").Result;
var tokenResult = tokenClient.RequestRefreshTokenAsync(refreshToken).Result;
if (tokenResult.IsError)
{
throw new Exception();
}
accessToken = tokenResult.AccessToken;
var idToken = HttpContext.GetTokenAsync("id_token").Result;
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = idToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = accessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = tokenResult.RefreshToken
}
};
var expiredAt = DateTime.UtcNow.AddSeconds(tokenResult.ExpiresIn);
tokens.Add(new AuthenticationToken
{
Name = "expires_at",
Value = expiredAt.ToString("o", CultureInfo.InvariantCulture)
});
var info = HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme).Result;
info.Properties.StoreTokens(tokens);
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, info.Principal, info.Properties).Wait();
_expiredAt = expiredAt.ToLocalTime();
}
return accessToken;
}
}
I call this method to get the access_token and add int to the API call headers:
private async getAuthenticationHeader(): Promise<any> {
return axios.get('config/accesstoken').then((response: any) => {
return { headers: { Authorization: `Bearer ${response.data}` } };
});
}
async getAsync<T>(url: string): Promise<T> {
return this.axios
.get(url, await this.getAuthenticationHeader())
.then((response: any) => response.data as T)
.catch((err: Error) => {
console.error(err);
throw err;
});
}
Double check locking were implemented to prevent simultamious async API calls try to change access_token at the same time. Optionally you can cashe you access_token into static variable or cache, it is up to you.
If you have any advices or alternatives it would be insteresting to discuss. Hope it helps someone.
There's 2 ways of doing this:
Client side - Handle the authentication and obtaining of the token on the client side using a lib like oidc-client-js. This has a feature that allows automatic renewal of the token via a prompt=none call to the authorize endpoint behind the scenes.
Refresh token - store this in your existing cookie and then use it to request a new access token as needed. In this mode your client side code doing the AJAX calls would need to be aware of token errors and automatically request a new token from the server whereby GetAccessTokenAsync() could use the refresh token to get a new access token.

issue with single sign on Azure active directory javascript library

We have single sign on enabled for our MS Dynamics 365 CRM instance to make a calls to an API hosted in Azure. On launch of CRM we have the following JavaScript that executes. This works most of the time, but on occasion we get "Invalid argument" popup. I am relatively new to using Adal.js and have no idea what is causing this. Any trouble shooting tips appreciated. Thanks in advance.
config = {
ApiUrl: configData["ApiUrl"],
SubscriptionKey: configData["SubscriptionKey"],
trace: configData["trace"],
AcceptHeader: configData["AcceptHeader"],
ContentTypeHeader: configData["ContentTypeHeader"],
tenant: configData["tenant"],
clientId: configData["clientId"],
tokenStoreUrl: configData["tokenStoreUrl"],
cacheLocation: configData["cacheLocation"],
GraphApi: configData["GraphApi"]
};
// Check For & Handle Redirect From AAD After Login
authContext = new window.AuthenticationContext(config);
var isCallback = authContext.isCallback(window.location.hash);
if (isCallback) {
authContext.handleWindowCallback();
}
var loginError = authContext.getLoginError();
if (loginError) {
console.log('ERROR:\n\n' + loginError);
}
authContext.popUp = true;
if (isCallback && !loginError) {
window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
}
var user = authContext.getCachedUser();
if (!user) {
authContext.clearCache();
sessionStorage["adal.login.request"] = "";
authContext.login();
}
window.parent.authContext = authContext;
It has been a while since I last looked at this, however I managed to get it resolved at the time. I implemented a locking mechanism, to ensure the login completes before trying to obtain a token.
Here is the updated code:
config = {
ApiUrl: configData["ApiUrl"],
SubscriptionKey: configData["SubscriptionKey"],
trace: configData["trace"],
AcceptHeader: configData["AcceptHeader"],
ContentTypeHeader: configData["ContentTypeHeader"],
tenant: configData["tenant"],
clientId: configData["clientId"],
tokenStoreUrl: configData["tokenStoreUrl"],
cacheLocation: configData["cacheLocation"],
GraphApi: configData["GraphApi"],
loadFrameTimeout: 10000
};
// Check For & Handle Redirect From AAD After Login
authContext = new window.AuthenticationContext(config);
var isCallback = authContext.isCallback(window.location.hash);
if (isCallback) {
authContext.handleWindowCallback();
}
var loginError = authContext.getLoginError();
if (loginError) {
// TODO: Handle errors signing in and getting tokens
console.log('ERROR:\n\n' + loginError);
}
authContext.popUp = true;
if (isCallback && !loginError) {
window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
}
var user = authContext.getCachedUser();
if (!user) {
authContext.clearCache();
sessionStorage["adal.login.request"] = "";
authContext.callback = function (error, token, msg) {
// remove lock
window.top.loginLock = null;
if (!!token) {
getGraphApiTokenAndUpdateUser(authContext);
}
else {
console.log('ERROR:\n\n' + error);
}
};
if (typeof (window.top.loginLock) == "undefined" || window.top.loginLock == null) {
// Create lock
window.top.loginLock = true;
authContext.login();
}
}
window.parent.authContext = authContext;

Meteor: Session problems

Im getting this error
TypeError: Cannot read property 'set' of undefined
Code is:
Router.map(function() {
this.route('/payment_return/:invoice_no/:amount/', {
where: 'server',
onBeforeAction: function() {
console.log("result");
result = paypal_return(this.params.invoice_no,this.params.amount,this.params.query.token,this.params.query.PayerID);
console.log(result);
if (result)
{
var tokens = this.params.amount*10;
console.log(tokens);
var playerId = this._id;
Session.set('selectedUser', playerId);
var selectedUser = Session.get('selectedUser');
Meteor.call('updateTokens', selectedUser, tokens);
this.response.end("Payment captured successfully");
}
else
{
this.response.end("Error in processing payment");
}
}
});
});
In, methods.js
Meteor.methods({
'updateTokens': function(selectedUser, tokens){
check(selectedUser, String);
check(tokens, Number);
var currentUserId = Meteor.userId();
if(currentUserId){
Meteor.users.update(selectedUser,
{ $inc: { 'profile.tokens': tokens}});
}
}
})
Basically, trying to update user's token amount after successful payment, but unfortunately it's returning just that error.
Sessions are only available in client side... Not sure where you are trying to call Session, but if Session package is included and you are calling Sessions.set/get on client it should work.
This looks like API call to me, so I will suggest you to use meteorhacks:picker
Then you can add on your server side:
var paymentRoutes= Picker.filter(function(req, res) {
return req.method == "POST"; //OR GET WHATEVER YOU NEED
});
paymentRoutes.route('/payment_return/:invoice_no/:amount/',
function(params, req, res, next) {
//UPDATE TOKEN
});
var paymentRoutes= Picker.filter(function(req, res) {
return req.method == "GET" || "POST";
});
paymentRoutes.route('/payment_return/:invoice_no/:amount/', function(params, req, res, next) {
result = paypal_return(params.invoice_no,params.amount,params.query.token, this.userId);
if (result){
var tokens = this.params.amount*10;
var playerId = this.userId;
Meteor.users.update({_id:playerId},{ $inc: { 'profile.tokens': tokens}});
res.end("Payment captured successfully");
}else{
res.end("Error in processing payment");
}
});
I hope this will be helpful, Cheers

How to return json web token (jwt) with passport-facebook without showing it in the redirect url

I am using passport-facebook to login in a MEAN stack webapp. After successful login, I want to generate a JSON Web Token (jwt) and redirect to a page in my SPA. (res.redirect('/#/posts/'+ doc.generateJWT()); -- please see the associated code below).
My question is:
How do I send the JWT to the redirect page without showing it in the URL?
Code:
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
});
passport.use(new FacebookStrategy({
clientID: FACEBOOK_APP_ID,
clientSecret: FACEBOOK_APP_SECRET,
callbackURL: FACEBOOK_CALLBACKURL
},
function(accessToken, refreshToken, profile, done) {
process.nextTick(function () {
User.findOne({'fbid':profile.id},function(err, docs) {
if (err){
//console.log('Error in SignUp: '+err);
return res.status(401).json(info);
}
else {
if (docs) {
//console.log('User already exists');
globalid = profile.id;
return done(null,docs);
} else {
// if there is no user with that fbid
// create the user
var newUser = new User();
// set the user's local credentials
newUser.fbid = profile.id;
globalid = profile.id;
newUser.firstname = profile.name.givenName;
newUser.lastname = profile.name.familyName;
newUser.gender = profile.gender;
if(profile.emails){
newUser.fbemail = profile.emails[0].value;
};
newUser.fblink = profile.profileUrl;
newUser.fbverified = profile.verified;
// save the user
newUser.save(function(err) {
if (err){
//console.log('Error in Saving user: '+err);
return res.status(401).json(info);
}
//console.log('User Registration succesful');
return done(null, newUser);
});
}
}
});
});
}));
var router = express.Router();
router.get('/auth/facebook',
passport.authenticate('facebook', { scope : 'email' }
));
router.get('/auth/facebook/callback',
passport.authenticate('facebook', { session: false, failureRedirect: '/'}),
function(req, res,done) {
var redirection = true;
User.findOne({ 'fbid': globalid }, function (err, doc){
//console.log("Generating token");
doc.token = doc.generateJWT();
doc.save(function(err) {
if (err){
//console.log('Error in Saving token for old user: '+err);
return res.status(401).json(info);
}
else
{
//console.log('User Login succesful');
redirection = doc.mobileverified;
//console.log(redirection);
//return done(null, doc);
if(doc.mobileverified === true){
console.log("Token:",doc.generateJWT());
res.redirect('/#/posts/'+ doc.generateJWT());
}
else{
console.log("Token:",doc.generateJWT());
//res.json({token: doc.generateJWT()});
res.redirect('/#/register/' + doc.generateJWT());
}
}
});
});
});
Many Thanks in advance!
If you don't wanna show your token on the url you have to send the response as json
var fbOptions = {
clientID: FACEBOOK_APP_ID,
clientSecret: FACEBOOK_APP_SECRET,
callbackURL: FACEBOOK_CALLBACKURL
};
passport.use(new FacebookStrategy(fbOptions, function(token, refreshToken, profile, done) {
var user = profile;
// NOTE: ‘my_token’ we will use later
user.my_token = 'generate your jwt token';
done(null, user);
}));
And then on your router return the token as json
app.get('/auth/facebook/callback', passport.authenticate('facebook', {session: false, failureRedirect : '/'}), function(req, res) {
// The token we have created on FacebookStrategy above
var token = req.user.my_token;
res.json({ token: token });
});

Resources