Step-up Authentication in Modern Applications

Step-up authentication in an application is a pattern of allowing access to non-critical resources using basic level of authentication, and requiring additional authentications for critical resources.


This is a companion discussion topic for the original entry at https://developer.okta.com/blog/2023/03/08/step-up-auth

I am following your post to implement Step-Up authentication but running into roadblocks. After login with username/password and okta verify, when user tries to access a risky transcation, i am asked for username and password again rather than just okta verify. Do you know what could be the reason. Below is my code.

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var passport = require('passport');
var qs = require('querystring');
var { Strategy } = require('passport-openidconnect');
const axios = require('axios');
const crypto = require('crypto');

// source and import environment variables
require('dotenv').config({ path: '.okta.env' })
//require('dotenv').config({ path: '.entra.env' })
const { ORG_URL, CLIENT_ID, CLIENT_SECRET, CALLBACK_URL } = process.env;

var indexRouter = require('./routes/index');
const { profile } = require('console');


var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use(session({
  secret: 'CanYouLookTheOtherWay',
  resave: false,
  saveUninitialized: true,
  // cookie: { secure: true, sameSite: false }
}));

// app.set("trust proxy", 1);

app.use(passport.initialize());
app.use(passport.session());

// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
let logout_url, id_token;
let _base = ORG_URL.slice(-1) == '/' ? ORG_URL.slice(0, -1) : ORG_URL;
console.log ('Base URL - ' + _base);
axios
  .get(`${_base}/.well-known/openid-configuration`)
  .then(res => {
    if (res.status == 200) {
      let { issuer, authorization_endpoint, token_endpoint, userinfo_endpoint, end_session_endpoint } = res.data;
      logout_url = end_session_endpoint;

      console.log ('Authorization Endpoint - ' +authorization_endpoint);

      const riskyActionInfo = {
        action: 'Login Button',
        referURL: '/',
        targetURL: '/profile'
     };
     
     //const state = JSON.stringify(riskyActionInfo);
     const state = JSON.stringify(riskyActionInfo);

      // Set up passport
      passport.use('oidc', new Strategy({
        issuer,
        authorizationURL: authorization_endpoint,
        tokenURL: token_endpoint,
        userInfoURL: userinfo_endpoint,
        clientID: CLIENT_ID,
        clientSecret: CLIENT_SECRET,
        callbackURL: CALLBACK_URL,
        scope: ['openid', 'profile', 'email'],
        state: true,
        nonce: true,
        acrValues:'urn:okta:loa:1fa:pwd',
        maxAge: 5,
      }, (issuer, profile, context, idToken, accessToken, refreshToken, params, done) => {
        console.log(`OIDC response: ${JSON.stringify({
          issuer, profile, context, idToken,
          accessToken, refreshToken, params
        }, null, 2)}\n*****`);
        id_token = idToken;
        return done(null, profile);
      }));
    }
    else {
      console.log(`Unable to reach the well-known endpoint. Are you sure that the ORG_URL you provided (${ORG_URL}) is correct?`);
    }
  })
  .catch(error => {
    console.error(error);
});

passport.serializeUser((user, next) => {
  next(null, user);
});

passport.deserializeUser((obj, next) => {
  next(null, obj);
});

function ensureLoggedIn(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login')
}

app.use('/', indexRouter);

app.use('/login', passport.authenticate('oidc'));

app.use('/authorization-code/callback',
  // https://github.com/jaredhanson/passport/issues/458

  passport.authenticate('oidc', { failureMessage: true, failWithError: true }),
  (req, res) => {
    console.log ("IN Callback Handler");
    const returnedState = req.query.state;
    let st = req.authInfo.state;
    console.log("State :" + st);

    if (st === undefined) {
      console.log("state is null or undefined");
      res.redirect('/profile');
    }
    else {
      console.log(JSON.parse(st).targetURL);
      var url = JSON.parse(st).targetURL;
      res.redirect(url);
    }
    
  }
);

app.use('/profile', ensureLoggedIn, (req, res) => {
  res.render('profile', { authenticated: req.isAuthenticated(), user: req.user, title: 'Profile' });
});

app.use('/secure_page', ensureLoggedIn, (req, res) => {
  res.render('secure_page', { authenticated: req.isAuthenticated(), user: req.user, title: 'Secure Page' });
});

app.use('/auth/2fa', ensureLoggedIn, (req,res) =>{
    console.log ("******In MFA Page******");
    const state = JSON.stringify({ action: 'secure_page', targetURL: '/secure_page' });
    const nonce = crypto.randomBytes(16).toString('hex');
    
    passport.authenticate('oidc', {
      scope: ['openid', 'profile', 'email','offline_access'],
      state:  state,
      nonce: nonce,
      acrValues: 'phrh',
      maxAge:'1',
  })(req, res);
});


app.get('/logout', (req, res) => {
  req.logout();
  req.session.destroy();
  res.redirect('/');
});



// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message + (err.code && ' (' + err.code + ')' || '') +
    (req.session.messages && ": " + req.session.messages.join("\n. ") || '');
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;```