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

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 });
});

Related

Configuring Custom Challenge to do token swap in AWS Cognito

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);
}

Next-Auth JWT Session Token

Currently I am returning the users email to the session as a JWT. This is working just fine. Now I am trying to return the users username with the email as a JWT. If I am to console.log the username, it is appearing in the terminal. Yet, if I am logging the session.user in my getServerSideProps function I am only able to view the email. Any tips?
export default NextAuth({
session: { jwt: true },
providers: [
CredentialsProvider({
async authorize(credentials) {
const client = await connectToDatabase();
const userCollection = client.db().collection("users");
const user = await userCollection.findOne({ email: credentials.email });
console.log(user.userName);
if (!user) {
client.close();
throw new Error("No user found!");
}
const isValid = await comparePasswords(
credentials.password,
user.password
);
if (!isValid) {
client.close();
throw new Error("Invalid password");
}
client.close();
if (user) {
return {
email: user.email,
userName: user.userName,
};
} else {
return null;
}
},
}),
],
});

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.

Passport.js Ajax login?

Is it possible to login via ajax with passport.js?
The thing is i'm creating a user via ajax and i want it to be logged in automatically (everything with json in a restful style) but the req.login() does some stuff that i don't know and that apparently sends its own status, headers and even it redirects to the home but and i need is to create my own json response.
The code where i create the user:
signup_facebook: function (req, res) {
var restponse = new Restponse();
var body = req.body;
var obj = {
display_name: body.first_name,
name: body.first_name,
surname: body.last_name,
photos: ['http://graph.facebook.com/'+ body.id+ '/picture?type=normal'],
gender: body.gender,
facebook: {
userID: body.id,
displayName: body.display_name
}
}
User.facebookSignUp(obj, function(user){
if(user !== false){
user = obj;
restponse.location = '/';
restponse.status = HTTPStatus.REST.C201_OK;
}else{
restponse.location = '/';
restponse.status = HTTPStatus.REST.C302_FOUND;
}
restponse.body = user;
req.login(user, {}, function(err) {
APIheart.respondJson(res, restponse);
});
})
Thanks for your time!
I found the answer in this post:
http://toon.io/on-passportjs-specific-use-cases/
// Manually establish the session...
req.login(user, function(err) {
if (err) return next(err);
return res.json({
message: 'user authenticated',
});
});

Retrieve Salted PW using Node-bcrypt and Passport.JS

I have an issue in which I can create hashed passwords in node-bcrypt and passport.js, but am unable to use the hashed password.
Im using nodejs, express, mongodb, mongoose, passport js, bcrypt.
What Im Trying To Do
Be able to login as normal, but using the bcrypt salted paswword etc.
What I have Done
I know my routes, api, and db are working. Since my current set up logs users in and out if I use a normal string for password instead of bcrypt.
I also checked my db and a bcrypt/salted password appears in the password field.
I got the idea to use bcrypt from this article (so using this code):
http://devsmash.com/blog/password-authentication-with-mongoose-and-bcrypt
Here is my relevant code:
var express = require('express'),
routes = require('./routes'),
passport = require('passport'),
util = require('util'),
flash = require('connect-flash'),
LocalStrategy = require('passport-local').Strategy,
mongoose = require('mongoose');
mongoose.connect('mongodb://54.254.96.11/bcrypt')
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
bcrypt = require('bcrypt'),
SALT_WORK_FACTOR = 10;
var user = new Schema({
username: { type: String, required: true, index: { unique: true } },
password: { type: String, required: true },
email: String
});
var user = mongoose.model('user', user);
//Bcrypt Code
user.pre('save', function(next) {
var guest = this;
// only hash the password if it has been modified (or is new)
if (!guest.isModified('password')) return next();
// generate a salt
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
if (err) return next(err);
// hash the password using our new salt
bcrypt.hash(guest.password, salt, function(err, hash) {
if (err) return next(err);
// override the cleartext password with the hashed one
guest.password = hash;
next();
});
});
});
user.methods.comparePassword = function(candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
if (err) return cb(err);
cb(null, isMatch);
});
};
//
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
user.findById(id, function (err, user) {
done(err, user);
});
});
passport.use(new LocalStrategy(
function(username, password, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
// Find the user by username. If there is no user with the given
// username, or the password is not correct, set the user to `false` to
// indicate failure and set a flash message. Otherwise, return the
// authenticated `user`.
user.findOne({ username: username}, function(err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false, { message: 'Unknown user ' + username }); }
if (user.password != password) { return done(null, false, { message: 'Invalid password' }); }
return done(null, user);
})
});
}
));
// Relevant Express Routes
app.post('/login',
passport.authenticate('local', { failureRedirect: '/login', failureFlash: true }),
function(req, res) {
res.redirect('/home');
});
app.post('/create', function(req, res, next){
var moot = new user({
"username": req.body.username,
"password" : req.body.password,
"email" : req.body.email});
moot.save(function (err) {
if (!err) {
res.redirect('/home');
}
else {
res.redirect('/');
}
});
});
I would do it this way:
create a new method for User model:
userSchema.statics.authenticate = function(username, password, callback)
{
this.findOne({username: username}, function(err, user)
{
if(err) return callback(err);
if(!user) return callback(null, false);
user.comparePassword(password, function(err, correct)
{
if(!correct) return callback(null, false);
callback(null, user);
});
});
}
then in the passport config:
passport.use(new LocalStrategy(
function(username, password, done)
{
User.authenticate(username, password, function(err, user)
{
if(err) return done(err);
if(!user) return done(null, false);
done(null, user);
}
}
));
This should work (I didn't test it)
PS: please use 'user' for one user
for the model, use 'User'

Resources