Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ app.post('/account/password', passportConfig.isAuthenticated, userController.pos
app.post('/account/delete', passportConfig.isAuthenticated, userController.postDeleteAccount);
app.post('/account/logout-everywhere', passportConfig.isAuthenticated, userController.postLogoutEverywhere);
app.get('/account/unlink/:provider', passportConfig.isAuthenticated, userController.getOauthUnlink);

/**
* API examples routes.
*/
Expand Down Expand Up @@ -367,3 +366,48 @@ To avoid this, set BASE_URL to the HTTPS endpoint and always access the app thro
});

module.exports = app;

/**
* Security routes (biometric auth, 2FA)
*/
const securityController = require('./controllers/security');

// Add these routes after your existing account routes
app.get('/account/security', passportConfig.isAuthenticated, securityController.getSecuritySettings);
app.get('/security/2fa/setup', passportConfig.isAuthenticated, securityController.getTwoFactorSetup);
app.post('/security/2fa/verify', passportConfig.isAuthenticated, securityController.postTwoFactorVerify);
app.post('/security/2fa/disable', passportConfig.isAuthenticated, securityController.postTwoFactorDisable);
app.get('/security/webauthn/register-options', passportConfig.isAuthenticated, securityController.getWebAuthnRegistration);
app.post('/security/webauthn/register', passportConfig.isAuthenticated, securityController.postWebAuthnRegistration);
app.get('/security/webauthn/auth-options', securityController.getWebAuthnAuthentication);
app.post('/security/webauthn/login', securityController.postWebAuthnLogin);
app.post('/security/webauthn/remove', passportConfig.isAuthenticated, securityController.postRemoveWebAuthnCredential);
app.post('/login/2fa/verify', securityController.postTwoFactorVerify);

// Update the login route to handle biometric login
app.get('/login/biometric', userController.getLogin);

// Biometric Auth Rate Limiter Config
const biometricLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per 15 minutes
standardHeaders: true,
legacyHeaders: false,
});

// Apply to biometric routes
app.post('/security/webauthn/login', biometricLimiter);
app.post('/security/webauthn/register', biometricLimiter);

/**
* Microsoft OAuth routes
*/
app.get('/auth/microsoft', passport.authenticate('microsoft'));
app.get('/auth/microsoft/callback', passport.authenticate('microsoft', { failureRedirect: '/auth/failure' }), (req, res) => {
res.redirect(req.session.returnTo || '/');
});

/**
* API examples routes - Add Microsoft to the list
*/
app.get('/api/microsoft', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getMicrosoft);
74 changes: 74 additions & 0 deletions config/passport.js
Original file line number Diff line number Diff line change
Expand Up @@ -903,3 +903,77 @@ exports.isAuthorized = async (req, res, next) => {

// Add export for testing the internal function
exports._saveOAuth2UserTokens = saveOAuth2UserTokens;

const MicrosoftStrategy = require('passport-oauth2').Strategy;
const User = require('../models/User');

// Add this to your existing passport configuration
passport.use(new MicrosoftStrategy({
clientID: process.env.MICROSOFT_APP_ID,
clientSecret: process.env.MICROSOFT_APP_SECRET,
callbackURL: `${process.env.BASE_URL}${process.env.MICROSOFT_CALLBACK_URL || '/auth/microsoft/callback'}`,
authorizationURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
scope: ['openid', 'profile', 'email', 'User.Read'],
state: true
}, async (accessToken, refreshToken, params, profile, done) => {
try {
// Microsoft returns user information in the params as an ID token
const idToken = params.id_token;
if (!idToken) {
return done(new Error('No ID token received from Microsoft'));
}

// Decode the ID token to get user information
const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString());

const {
sub: id,
email,
name,
given_name: firstName,
family_name: lastName,
picture
} = payload;

// Check if user already exists by Microsoft ID
let user = await User.findOne({ microsoft: id });

if (user) {
return done(null, user);
}

// Check if user exists by email (for account linking)
user = await User.findOne({ email: email.toLowerCase() });

if (user) {
// Link Microsoft account to existing user
user.microsoft = id;
user.tokens.push({ kind: 'microsoft', accessToken });
await user.save();
return done(null, user);
}

// Create new user
const newUser = new User({
email: email.toLowerCase(),
profile: {
name: name,
gender: '',
location: '',
website: '',
picture: picture || ''
},
microsoft: id,
tokens: [{ kind: 'microsoft', accessToken }]
});

await newUser.save();
done(null, newUser);
} catch (error) {
done(error);
}
}));

// Add Microsoft to the list of authorized providers
const authorizedProviders = ['github', 'facebook', 'twitter', 'linkedin', 'google', 'twitch', 'microsoft', 'discord', 'tumblr', 'steam', 'quickbooks', 'trakt'];
58 changes: 58 additions & 0 deletions config/webauth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } = require('@simplewebauthn/server');
const { isoBase64URL, isoUint8Array } = require('@simplewebauthn/server/helpers');

class WebAuthnConfig {
constructor() {
this.rpID = process.env.DOMAIN || 'localhost';
this.rpName = process.env.APP_NAME || 'Express Starter';
this.origin = process.env.BASE_URL || `http://localhost:${process.env.PORT || 8080}`;
}

async generateRegistrationOptions(user) {
return await generateRegistrationOptions({
rpName: this.rpName,
rpID: this.rpID,
userID: user._id.toString(),
userName: user.email,
userDisplayName: user.profile.name || user.email,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
},
});
}

async generateAuthenticationOptions() {
return await generateAuthenticationOptions({
rpID: this.rpID,
userVerification: 'preferred',
});
}

async verifyRegistrationResponse(attestationResponse, expectedChallenge) {
return await verifyRegistrationResponse({
response: attestationResponse,
expectedChallenge,
expectedOrigin: this.origin,
expectedRPID: this.rpID,
});
}

async verifyAuthenticationResponse(assertionResponse, expectedChallenge, credential) {
return await verifyAuthenticationResponse({
response: assertionResponse,
expectedChallenge,
expectedOrigin: this.origin,
expectedRPID: this.rpID,
authenticator: {
credentialID: isoBase64URL.toBuffer(credential.credentialID),
credentialPublicKey: isoUint8Array.fromHex(credential.publicKey),
counter: credential.counter,
transports: credential.transports,
},
});
}
}

module.exports = new WebAuthnConfig();
53 changes: 53 additions & 0 deletions controllers/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -1551,3 +1551,56 @@ exports.getPubChem = async (req, res, next) => {
next(error);
}
};

const axios = require('axios');

/**
* GET /api/microsoft
* Microsoft Graph API example.
*/
exports.getMicrosoft = async (req, res, next) => {
try {
const token = req.user.tokens.find(token => token.kind === 'microsoft');

if (!token) {
req.flash('errors', { msg: 'You must link your Microsoft account first.' });
return res.redirect('/api');
}

// Get user profile from Microsoft Graph
const profileResponse = await axios.get('https://graph.microsoft.com/v1.0/me', {
headers: {
Authorization: `Bearer ${token.accessToken}`,
'Content-Type': 'application/json'
}
});

// Get user photo if available
let photoUrl = null;
try {
const photoResponse = await axios.get('https://graph.microsoft.com/v1.0/me/photo/$value', {
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
responseType: 'arraybuffer'
});

const photoBase64 = Buffer.from(photoResponse.data, 'binary').toString('base64');
photoUrl = `data:${photoResponse.headers['content-type']};base64,${photoBase64}`;
} catch (photoError) {
console.log('No profile photo available or error fetching photo:', photoError.message);
}

res.render('api/microsoft', {
title: 'Microsoft Graph API',
profile: profileResponse.data,
photoUrl
});
} catch (error) {
if (error.response && error.response.status === 401) {
req.flash('errors', { msg: 'Your Microsoft token has expired. Please link your account again.' });
return res.redirect('/api');
}
next(error);
}
};
Loading