Securing a PowerShell Web App Using Azure AD

I have a new use case for a web app. I’ve been searching all over for a good knowledgebase that I can use to store my documentation of the systems I administrate and end user self service articles. While I solved the problem at work using two different packages (sigh…) I also got interested in what it would take to write my own, which is way more than I have time to do at work, which is why I started doing it as a hobby in my free time. This post may be the first of many as I develop this.

In essence, I started by building on this: Building a Modern API Using Powershell.

Before I get to deep into this I started by setting up my PODE instance with a folder structure like this:

>Functions
    Functions.ps1
>Public
    >Script
    >Style
        >Fonts
        >Images
>Routes
    >API
        >Beta
        >V1
    sessiondata.ps1
    index.ps1
    admin.ps1
api.ps1
server.psd1

As I begin the important files in the above list are the ones with a file extension and I’ll touch on each of them as it makes sense.

For this project I’ve decided to use AzureAD Authentication. All the secrets for Azure Authentication, and the local authentication to my DB will be stored in the server.psd1 file, which is a PowerShell data file. A psd1 file essentially stores things in a hash table. PODE has a built in function for accessing the file server.psd1 called get-podeconfig which we will use later. For security purposes this file can’t be checked into GIT or shared here in it’s true form, but it otherwise sort of looks like this:

@{
    Auth=@{
        ClientID=''
        ClientSecret=''
        Tenant =''
    }
    sqlsettings=@{
        database=""
        datasource=""
        username=''
        password=''
    }
}

The API.ps1 file

The api.psi file is the main ‘PODE’ file where it instantiates the endpoint and then pulls in all the other files.

Since this is still early development and I just want to get my identity check up and running early, I opt for logging to the terminal and using a localhost and local port endpoint.

add-podeendpoint -address * -port 8081 -protocol Http
New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging

To setup Azure AD authentication as my identity manager in PODE is fairly easy, however I also decided I wanted to grab a user’s picture from the Microsoft graph API and that complicated things a little.

# setup authentication to validate a user
    $auth=(get-podeconfig).Auth #get-podeconfig reads from the server.psd1 file
    $scheme = New-PodeAuthAzureADScheme @auth

    $scheme | Add-PodeAuth -Name 'Azure' -FailureUrl '/?login=failed' -SuccessUseOrigin -ScriptBlock {
        param($user, $accessToken, $refreshToken, $response)
        #build a header to send to MS to get the user's picture
        $headers = @{ 
            'Content-Type' = 'application/json'
            Accept = 'application/json'
            Authorization = "Bearer $accesstoken" 
        }
        #get the MS uri for the user's picture
        $uri=$user.picture
        $tempfile=new-temporaryfile
        #get the user's picture and put it into the $user object
        try{
            invoke-restmethod -uri $uri -headers $headers -method get -outfile $tempfile
            $bytes=[System.IO.File]::ReadAllBytes($tempfile)
            $picture=[Convert]::ToBase64String($bytes)
        }catch{$picture=$null}
        $user.picture=$picture
        #return the user object to the rest of PODE
        return @{ User = $user}
    }

Let me digress for a moment about Identity Management vs User Authorization. Here in PODE I am using Azure AD for Identity Management. it’s simply going to tell my app that yes, this is User X. The database for the knowledgebase is going to require a user’s table which has appropriate access controls, permissions and roles. Building on the DB, the API calls will also need to have proper access controls. The web pages, well maybe they need access controls, but they may not. There are some pages that I do want to allow anonymous user access, but perhaps display more information if the user is logged in, or even if a particular user, with a certain role is logged in.

I also want to take a moment to point out that PODE will automatically create a route ‘oauth2/callback’ which is what Azure will call back to during the authentication process. At this point in the process is when I would normally go log into my Azure tenant and create the app registrations for this app that I am building.

For the Identity Management process to work right we need a login and logout pages. The ‘login’ page doesn’t need anything special under the hood, PODE handles redirecting the user to the Microsoft login page. While PODE doesn’t strictly require the logout page to do anything either, I have mine set to clear the PODE created cookies so it actually will close the user’s sessions and behave like a real logout function.

    # login - this will just redirect to azure
    Add-PodeRoute -Method Get -Path '/login' -Authentication Azure

    # logout
    Add-PodeRoute -Method get -Path '/logout' -Authentication Azure -Logout -scriptblock{
        remove-podecookie -name 'pode.sid'
        remove-podecookie -name 'cwawsid'

        Move-PodeResponseUrl -Url "/?logout=true"
    }

The API.ps1 file ends with me calling the PODE functions to include all the routes I’m going to setup and the functions I plan to use. I also opt to enable the openAPI function of PODE since I know it will be useful for troubleshooting later when I actually start writing API calls.

    #read in all the files in the routes folders
    use-poderoutes

    #load the functions scripts
    use-podescript -path "$basepath\functions"

    Enable-podeopenapi -path '/docs/openapi' -OpenApiVersion '3.0.3' #-DisableMinimalDefinitions -EnableSchemaValidation
    add-podeoainfo -version '1.0.0' -title 'Pode Knowledgebase'
    enable-podeopenapiviewer -type ReDoc -path '/docs/redoc'
    enable-podeopenapiviewer -type Swagger -path '/docs/swagger'
    enable-podeopenapiviewer -bookmarks -path '/docs'

Functions.ps1

For now, my functions.ps1 file has a single function, get-user, which will return the local user info when given a email address retrieved from Azure. Eventually I plan on having individual function files for each logical grouping, IE a UserFunctions.ps1, ArticleFunctions.ps1, etc.

Index, Sessiondata and Admin pages.

Every time I setup a PODE instance I always start by creating three pages. While the names aren’t important, the authorization to them is. The index page lives at the root of the site and is set to be accessible without authorization.

#Define Route Meta
$RoutePath = '/'
#end Define Route Meta

add-poderoute -method get -path $routepath -scriptblock{
#define route logic
$html=@"
<head>
    <link rel="stylesheet" type="text/css" href="style/default.css" />
    <script src="script/knowledgebase.js"></script>
</head>
<body>
    <a href="/">Front page</a><br>
    <a href="/login">login page</a><br>
    <a href="/admin">admin page</a><br>
    <a href="/docs">API Documentation Page</a><br>
</body>
"@

    write-podehtmlresponse -value $html
#end route logic
}

For sessiondata and admin, they are basically clones of each other, except admin requires a successful login and sessiondata is optional. The real difference being the route path and the allowanon switch when we call add-poderoute. For brevity I’ll post the sessiondata.ps1 file:

#Define Route Meta
$RoutePath = '/sessiondata'
#end Define Route Meta

add-poderoute -method get -path $routepath -authentication Azure -allowanon -scriptblock{
#define route logic
    write-podehost $(convertto-json $webevent.request -depth 5)
    $db=(get-podeconfig).sqlsettings.database
    $ds=(get-podeconfig).sqlsettings.datasource
    #get local user ID#
    $userdata=get-user $ds $db -email $webevent.session.data.auth.user.email
    $html=@"
<head>
    <link rel="stylesheet" type="text/css" href="style/default.css" />
    <script src="script/knowledgebase.js"></script>
</head>
<body>
    Session data: Username:  $($webevent.session.data.auth.user.name) Email: $($webevent.session.data.auth.user.email)<br>
    User Info From System: $($userdata|convertto-json)<br>
    <img src="data:image/jpeg;base64,$($webevent.session.data.auth.user.picture)"><br>
    <a href="/">Front page</a><br>
    <a href="/login">login page</a><br>
    <a href="/admin">admin page</a><br>
    <a href="/docs">API Documentation Page</a><br>
</body>
"@

    write-podehtmlresponse -value $html
#end route logic
}

Assuming everything is setup properly in the Azure tenant, once the PODE server is started, I should be able to browse to the index page, go to the session data, see that I am not logged in, proceed to the Admin page, which will send me through the Azure login process and then return me to the admin page where I can see that I am logged in and the details of my user account returned by my get-user function. Setting up everything in Azure and the API.ps1 should now be complete and now I can get started on the rest of the web app.

At the time of this posting I’m not quite ready to share what I’ve been working on, so unfortunately I don’t have any git links where you can go download the code, but that may change as the project develops.