Wednesday, December 25, 2013

Reauthentication after Timeout

I have a requirement at the application level to lock the screen and force the user to re-authenticate after a specific period of inactivity. This solves the issue of a user going to lunch in the middle of a transaction and someone else sneaking into their cube and doing bad things. It would be easier to simply log them out after a period of inactivity, but then they would lose all their unsaved work.

The obvious way to do this is to use a timer which gets reset every time the user does something (we haven't decided what 'something' is). When the timer reaches the timeout (say 10 minutes) we would lock the screen and force re-authentication.

Although the screen is technically locked when we bring up the authentication modal dialog box, we want to display a grey, translucent panel over the screen because if we don't our users might get confused. Why we assume our users are so stupid, I will never understand. I've met them, they're not stupid.

We seem to spend 10% of our resources idiot proofing our applications for 1% of the users. Wouldn't it make sense to fire the 1% and hire someone else? I'd rather spend that 10% creating wizzo features for the 10% of the smartest users. We have a super smart user, let's call her "Faith". She has lot's of good ideas but we can't do most of them because we're spending so much time idiot proofing the application for someone in Accounting, let's call him "Fear". But I digress.

So I started by using a timers.timer but apparently that's not the right thing to do in WPF. We should use a DispatcherTimer. In my example I have a single textbox. Whenever the user types in the textbox I reset the timer. In reality you would have many textboxes so rather than attach a KeyDown handler to each one you would attach a single handler at the window. Simply attach the events that qualify as "activity". Because RoutedEvents bubble and tunnel, the window will eventually see the event, unless a lower level handler marked it as handled. Or you could use the Preview events which tunnel so the Window sees them first.

Note: If you attach the Window's KeyDown event handler in code, there is an option for the window to see it, even if a prior handler marked it as handled. This option is not available in XAML.

I chose to show/hide the protective panel by altering its size. I could have achieved the same result using the opacity or zindex. I set the timeout to two seconds for the example.

I used the Stop command in the example for simplicity but in reality I would create a custom RoutedCommand to do this.

I destroy the timer in the Finalize event handler to avoid memory leaks.


<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525" TextBox.KeyDown="Window_KeyDown">
    <Window.CommandBindings>
        <CommandBinding Command="Stop" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"/>
    </Window.CommandBindings>
    <Grid Name="LayoutGrid">
        <TextBox Name="TextBox" Width="100" Height="20" Background="Azure"/>
        <Label Name="ProtLabel" Background="gray" Opacity=".5" Width="0" Height="0"/>
    </Grid>
</Window>


Imports System.Windows.Threading
Class MainWindow

    Protected t As DispatcherTimer
    Public Sub New()
        InitializeComponent()

        ' Create and start the timer
        t = New DispatcherTimer()
        t.Interval = TimeSpan.FromSeconds(2)
        AddHandler t.Tick, AddressOf Timeout
        t.Start()
    End Sub

    Private Sub CommandBinding_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
        e.CanExecute = True
        e.Handled = True
    End Sub

    Private Sub CommandBinding_Executed(sender As Object, e As ExecutedRoutedEventArgs)
        ' Stop the timer, protect the page, ask for authentication
        t.Stop()
        ProtLabel.Width = LayoutGrid.ActualWidth
        ProtLabel.Height = LayoutGrid.ActualHeight
        If MessageBox.Show("Are you who you say you are?", "Logon", MessageBoxButton.YesNo) = MessageBoxResult.No Then
            Me.Close()
        End If
        ' User authenticated so start the timer and unprotect the page
        t.Start()
        ProtLabel.Width = 0
        ProtLabel.Height = 0
    End Sub

    Private Sub Timeout()
        ApplicationCommands.Stop.Execute(Nothing, Nothing)
    End Sub

    Private Sub Window_KeyDown(sender As Object, e As KeyEventArgs)
        ' Reset the timer
        t.Stop()
        t.Start()
    End Sub

    Protected Overrides Sub Finalize()
        ' Destroy the timer
        MyBase.Finalize()
        t.Stop()
        t = Nothing
    End Sub
End Class

No comments:

Post a Comment