Pode icon indicating copy to clipboard operation
Pode copied to clipboard

Authorization Middleware

Open Viajaz opened this issue 3 years ago • 3 comments

Describe the Feature

There already exists inbuilt middleware for Authentication but having inbuilt middleware for Authorization of Routes would be useful as well, possibly using a RBAC model.

  • ScriptBlock for obtaining a users (After Auth middleware) roles.
  • Labelling a route with role(s) at creation of the route or or dynamically when needed via ScriptBlock.
  • Enforcement of RBAC on relevant routes.

Viajaz avatar Jun 23 '22 03:06 Viajaz

I'm doing something similar by using custom Authentication and I've basically rebuilt something that allows me to verify the JWT etc. That gives me access to the audience/clientid and I can restrict each route dynamically, based on the config file. Are you asking for an in-route restriction? For example, you have a route that returns a list from a database and you're allowing access to your route to both ClientID1 and ClientID2 but ClientID2 will have a specific filtering in place that will force the query to start with "abc", thus returning only abc results? I'm asking because I'm curious to see how others would solve this. For a specific route, I've built it within the global config file I created to autobuild routes and I've added the logic in-route. For authentication itself, I have an array of clientids that are allowed in the route and the custom auth flow looks at that to see if you're authorized.

scorbisiero avatar Jun 23 '22 07:06 scorbisiero

Hi @Viajaz,

This is a good idea! I'm wondering if we could add a New-PodeAuthAccess function, and this takes a scriptblock which is passed the $user from the initial authentication. You then return the Roles for that user (either from some location on the user object, or retrieved from elsewhere)

The Access object can then be bound to an Add-PodeAuth, and will be run after the authentication takes place. If the Route has no -Roles defined then access is allowed, but if there are -Roles then the user is only allowed access if their Roles are in the Route's roles.

Something like below perhaps? 🤔 Where "Morty" can only access /users2.

# setup rbac auth
$rbac = New-PodeAuthAccess -Name 'ExampleRbac' -Type RBAC -ScriptBlock {
    param($user)
    return $user.Metadata.Roles
}

# setup basic auth, using above rbac
New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Validate' -Sessionless -Access $rbac -ScriptBlock {
    param($username, $password)

    return @{ User = @{
        Name = 'Morty'
        Metadata = @{ Roles = @('User') }
    } }
}

# route which uses above auth, and defines Admin role to access
Add-PodeRoute -Method Post -Path '/users1' -Authentication 'Validate' -Role 'Admin' -ScriptBlock {
    # logic
}

# route which uses above auth, and defines Admin and USer role to access
Add-PodeRoute -Method Post -Path '/users2' -Authentication 'Validate' -Role 'Admin', 'User' -ScriptBlock {
    # logic
}

New-PodeAuthAccess could have a -Type, so we might support GBAC/etc in the future.


As for in-route restriction, the ways I've seen people do it, and that I've done myself, is to just do the check wihtin the route:

if ($user.role -eq 'something') {
    # this
}
else {
    # this
}

# etc.

But with the RBAC features above, if we add an Access.Roles property to $WebEvent.Auth then maybe we could do a Test-PodeAuthAccess or similar 🤔. Something like this maybe:

if (Test-PodeAuthAccess -Role 'User') {
    # this
}
else {
    # this
}

# etc.

Or possibly a way of creating role based scriptblocks 🤔

Badgerati avatar Jun 23 '22 20:06 Badgerati

That would be cool. I currently partially solved this by using a custom Auth flow and have all of the data needed for the route in a config, which I retrieve when auth is called: $RouteAuthInfo = ($using:Config).Routes | Where-Object { $_.Authentication.Enabled -and $_.Path -eq (($WebEvent.Route.Path).replace("(?<", ":").replace(">[^\/]+?)", "")) }

After a stupid check on the above, I grab just this: $RouteAuthInfo = $RouteAuthInfo.Authentication

Finally, down the line, if the JWT has passed all that was needed, I simply check if the client ID is in my list, in the config: $JWT.appid -in $RouteAuthInfo.ClientIDs.ID

scorbisiero avatar Jun 24 '22 09:06 scorbisiero

I think this issue same as / expand on https://github.com/Badgerati/Pode/issues/735 Anyway I taught that this already existed as I was using Pode.Web and didn't realized it doesn't come from Pode. Should we essentially move Test-PodeWebPageAccess from Pode.Web Add-PodeWebPage to Pode Add-PodeRoute/Add-PodeStaticRoute (presumably with a new middleware)? https://github.com/Badgerati/Pode.Web/blob/f6fa2047c645d2cb095c955a03be5150c23d1fab/src/Public/Pages.ps1#L505-L508

ili101 avatar Mar 15 '23 18:03 ili101

That's the plan!

For Pode.Web I handcranked it into the module, but if we add in the support directly to Pode properly we can update the way Pode.Web handles it - and remove the custom logic.

And yes, it's an extension to #735 😄

I've a plan for the next Pode and Pode.Web releases, where they'll be linked and will help enhance each other - with features like this one.

Badgerati avatar Mar 16 '23 18:03 Badgerati

I've been working on the Authorisation feature and functions. It works similar to the idea posed above, but with a couple of tweaks. Instead of New-PodeAuthAccess it's Add-PodeAuthAccess, and the -Access on Add-PodeAuth takes an array of Access method Names instead of raw objects.

When the user is authenticated, it automatically attempts authorisation. For the authorisation there are 3 inbuilt models for Roles, Groups and Scopes, but you can build a Custom access method if required. Routes have a -Role, -Scope, and -Group parameters, and for building custom ones there's Add-PodeAuthCustomAccess to pipe Routes into.

If the user is authorised, then a $WebEvent.Auth.IsAuthorised property is set to $true - otherwise $false. If the user fails authorisation then a HTTP 403 is returned, and the request terminated.

Example:

Start-PodeServer -Threads 2 {
    Add-PodeEndpoint -Address * -Port 8085 -Protocol Http

    # setup RBAC
    Add-PodeAuthAccess -Type Role -Name 'TestRbac'

    # setup basic auth
    New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Validate' -Access 'TestRbac' -Sessionless -ScriptBlock {
        param($username, $password)

        # check a dummy user, who only had "Developer" Role assigned
        if ($username -eq 'morty' -and $password -eq 'pickle') {
            return @{ User = @{ Roles = @('Developer') } }
        }

        return @{ Message = 'Invalid details supplied' }
    }

    # Route with no Roles, so any auth'd user can access
    Add-PodeRoute -Method Post -Path '/users-all' -Authentication 'Validate' -ScriptBlock {
        Write-PodeJsonResponse -Value @{
            Users = @( @{ Name = 'Deep Thought', Age = 42 } )
        }
    }

    # Route with Developer Role only
    Add-PodeRoute -Method Post -Path '/users-dev' -Authentication 'Validate' -Role Developer -ScriptBlock {
        Write-PodeJsonResponse -Value @{
            Users = @( @{ Name = 'Leeroy Jenkins', Age = 1337 } )
        }
    }

    # Route with Admin Role only
    Add-PodeRoute -Method Post -Path '/users-admin' -Authentication 'Validate' -Role Admin -ScriptBlock {
        Write-PodeJsonResponse -Value @{
            Users = @( @{ Name = 'Arthur Dent', Age = 30 } )
        }
    }

}

Calling the following will fail with 403:

Invoke-RestMethod -Uri http://localhost:8085/users-admin -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }

Calling either of the following will succeed:

Invoke-RestMethod -Uri http://localhost:8085/users-all -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }
Invoke-RestMethod -Uri http://localhost:8085/users-dev -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }

I also need to test how this will pair with Pode.Web, as this will replace the authorisation written over there.

Badgerati avatar Aug 14 '23 20:08 Badgerati