Skip to content
This repository was archived by the owner on Dec 8, 2021. It is now read-only.

Commit a85c31c

Browse files
Tiberriver256TylerLeonhardt
authored andcommitted
parent e036be7 (#192)
author Micah Rairdon <micah.rairdon@Haworth.com> 1563728062 -0400 committer Micah Rairdon <micah.rairdon@Haworth.com> 1564663426 -0400 Starting a draft Properly set header values instead of overwrite Allow responses to be sent from middleware Corrected script block tags Finished up the authentication documentation draft Properly set header values instead of overwrite Allow responses to be sent from middleware Corrected script block tags Finished up the authentication documentation draft
1 parent e036be7 commit a85c31c

File tree

7 files changed

+205
-62
lines changed

7 files changed

+205
-62
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Add any other context about the problem here.
4040

4141
Run the following script and copy and paste the results here:
4242

43-
```ps
43+
```powershell
4444
$Version = [pscustomobject]$PSVersionTable
4545
$Version.PSCompatibleVersions = ($Version.PSCompatibleVersions | foreach { "$($_.Major).$($_.Minor).$($_.Build).$($_.Revision)" }) -join ", "
4646
(Get-Module Polaris | select Name,Version | ConvertTo-Html -Fragment | Out-String) + ($Version | ConvertTo-Html -Fragment | Out-String)

.vscode/settings.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
{
2-
"powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1",
3-
"editor.formatOnSave": true
2+
"powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1"
43
}

docs/about_Authentication.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
layout: default
3+
title: Authentication
4+
type: about
5+
---
6+
7+
# Authentication
8+
9+
## about_GettingStarted
10+
11+
# SHORT DESCRIPTION
12+
13+
Authentication is verifying who the consumer of your service or site is. Polaris uses the .Net class [System.Net.HttpListener](https://docs.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=netframework-4.8) under the hood and is thus able to support the following authentication schemes out of the box:
14+
15+
| Authentication Scheme | Description |
16+
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
17+
| Basic | Specifies basic authentication. |
18+
| Digest | Specifies digest authentication. |
19+
| IntegratedWindowsAuthentication | Specifies Windows authentication. |
20+
| Negotiate | Negotiates with the client to determine the authentication scheme. If both client and server support Kerberos, it is used; otherwise, NTLM is used. |
21+
| NTLM | Specifies NTLM authentication. |
22+
| Anonymous (Default) | Specifies anonymous authentication. |
23+
24+
These authentication methods are global and are declared when you are creating your Polaris object as follows:
25+
26+
```powershell
27+
# Basic Authentication
28+
Start-Polaris -Auth Basic
29+
30+
# Ntml
31+
Start-Polaris -Auth NTLM
32+
```
33+
34+
User information will be available for use inside your scriptblocks via `$Request.User`:
35+
36+
```powershell
37+
Start-Polaris -Auth Basic
38+
39+
New-PolarisRoute -Method GET -Path "/whoami" -Scriptblock {
40+
$Response.json(($Request.User | ConvertTo-Json))
41+
}
42+
```
43+
44+
Note that with Basic auth all you get is `$Request.User.Identity.Name` and `$Request.User.Identity.Password`. If you are using Basic authentication in any web application you are going to have to write additional logic to validate that username and password are correct and determine whether or not they are authorized for different parts of your application.
45+
46+
The easiest way to implement authentication is if you are in a Windows environment hosting Polaris from a Windows server and are authenticating with domain joined user accounts. If this is the case authentication and authorization are as simple as:
47+
48+
```powershell
49+
Start-Polaris -Auth Negotiate
50+
51+
# Here we are checking to see if the user is in the administrator group of the PC where Polaris is hosted from
52+
# Note: This will return false unless you access Polaris from an elevated web browser
53+
New-PolarisRoute -Method Get -Path "/TestAdminGroup" -Scriptblock {
54+
$Response.Send("User is in Administrators Role: $($Request.User.IsInRole('Administrators'))")
55+
}
56+
57+
# You can also use IsInRole to test if the user is in any Active Directory security group
58+
New-PolarisRoute -Method Get -Path "/TestADSecurityGroup" -Scriptblock {
59+
$MyActiveDirectorySecurityGroup = "SecurityGroup1"
60+
$Response.Send("User is in $MyActiveDirectorySecurityGroup Role: $($Request.User.IsInRole($MyActiveDirectorySecurityGroup))")
61+
}
62+
```
63+
64+
This is also a very nice experience for end users as on Windows PCs they will not be prompted for credentials unless their PC cannot talk to a domain controller (i.e. off network).
65+
66+
## Additional Authentication Types
67+
68+
Any additional or custom authentication types can be implemented using the `New-PolarisRouteMiddleware`.
69+
70+
# LONG DESCRIPTION
71+
72+
## Basic
73+
74+
Basic authentication strictly validates that a client has passed Polaris a header that looks like this: `Authorization: Basic <Base64Encoded(username:password)>. If the incoming Http request does not have the required headers it will send a response to the client telling them that Basic authentication is required. You can read more about basic authentication on the [Mozilla docs site here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
75+
76+
One thing to remember with this authentication scheme though is that it only ensures there is a username and password. Not that the username and password is valid. This leaves it up to you the application developer to validate the username and password.
77+
78+
The most basic implementation of this would be a simple hard coded username and password inside of a Middleware like this:
79+
80+
```powershell
81+
New-PolarisRouteMiddleware -Name "BasicAuthValidation" -Scriptblock {
82+
if($Request.User.Identity.Name -ne "Username" -or $Request.User.Identity.Password -ne "Password") {
83+
$Response.StatusCode = 401;
84+
$Response.Send("Unauthorized")
85+
}
86+
}
87+
```
88+
89+
Of course in a production environment you might want to use the username and password to query against a table in a database.
90+
91+
## Digest
92+
93+
Digest authentication is an authentication method that can be used to avoid sending the username and password in clear text. With HTTPS being easy and very prevalent this is less of a need for sites who want to use just username and password for authentication.
94+
95+
You can read more about digest authentication in [the official RFC](https://tools.ietf.org/html/rfc2617#section-3)
96+
97+
## Integrated Windows Authentication & Negotiate & NTLM
98+
99+
All three of these authentication methods are very useful for internal web applications that are being accessed by computers or users on the same domain as the computer that is hosting the web application. Typically you will want to use `Negotiate` as that will ensure the best compatibility with any client that is going to be connecting with Polaris.
100+
101+
It will populate `$Request.User` with an IPrincipal that contains all the Active Directory security groups the authenticated user is a member of. You can test their membership of security groups using `$Request.User.IsInRole`. It can also be used to impersonate the user if Polaris is being executed as a service account that has authorization to impersonate authenticated users by `[WindowsIdentity]::RunImpersonated($Request.User.Identity.AccessToken, [action]{ & whoami })`.
102+
103+
Impersonation offers an excellent solution if you are attempting to create a web based file server as access to the files and folders can be managed using standard file and folder permissions and impersonation will ensure that those security rules are honored.
104+
105+
## Custom Authentication
106+
107+
Since this is not supported out of the box and needs to be implemented in a middlware you will need to start Polaris with `Start-Polaris -Auth Anonymous` or leave Auth unspecified and it will default to Anonymous.
108+
109+
Then authentication can be completely implemented using middleware. Here is an example of implementing Basic authentication as a middleware:
110+
111+
```powershell
112+
New-PolarisRouteMiddleware -Name "BasicAuthValidation" -Scriptblock {
113+
$AuthHeader = $Request.Headers.Get("Authorization")
114+
115+
if($AuthHeader -eq $Null -or $AuthHeader -notmatch "^Basic") {
116+
$Response.Headers.Add("WWW-Authenticate", "Basic realm=$Env:Computername")
117+
$Response.StatusCode = 401
118+
$Response.Send("Unauthorized. Missing auth header.")
119+
}
120+
121+
$DecodedHeader = [system.text.encoding]::UTF8.GetString([system.convert]::FromBase64String(($Request.Headers.Get("Authorization") -split " ")[1]))
122+
$Username = ($DecodedHeader -split ":")[0]
123+
$Password = ($DecodedHeader -split ":")[1]
124+
125+
if($Username -ne "Username" -or $Password -ne "Password") {
126+
$Response.Headers.Add("WWW-Authenticate", "Basic realm=$Env:Computername")
127+
$Response.StatusCode = 401;
128+
$Response.Send("Unauthorized. Incorrect username or password.")
129+
}
130+
}
131+
```
132+
133+
It is a recommended best practice if you want to store the user's roles and access them throughout the rest of the application to create an IUserPrinciple and assign it to `$Request.User`.

docs/about_GettingStarted.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type: about
1212

1313
Here is a super simple Polaris application to get you started
1414

15-
```ps
15+
```powershell
1616
Import-Module Polaris
1717
New-PolarisGetRoute -Path "/" -Scriptblock {
1818
$Response.Send('Hello World!')
@@ -30,7 +30,7 @@ I can get a response from the server by either opening a browser to <http://loca
3030

3131
Command:
3232

33-
```ps
33+
```powershell
3434
PS> Invoke-RestMethod -Method GET -Uri http://localhost:8080/
3535
```
3636

@@ -50,7 +50,7 @@ For the first one our route to the page will be "/Services" and we'll just want
5050

5151
We'll create the Polaris route like this
5252

53-
```ps
53+
```powershell
5454
New-PolarisGetRoute -Path "/Services" -Scriptblock {
5555
$RunningServices = Get-Service | Select-Object Name,DisplayName,Status | ConvertTo-Html -Title "Services" | Out-String
5656
$Response.SetContentType("text/html")
@@ -62,7 +62,7 @@ We're leveraging PowerShell's built in ConvertTo-HTML command to generate a litt
6262

6363
The second one we'll want to follow suit and create a route for the page called "/Processes" and display the Process Name, CPU, and the Id properties of the process.
6464

65-
```ps
65+
```powershell
6666
New-PolarisGetRoute -Path "/Processes" -Scriptblock {
6767
$RunningProcesses = Get-Process | select ProcessName,CPU,Id | ConvertTo-Html -Title "Processes" | Out-String
6868
$Response.SetContentType("text/html")
@@ -82,31 +82,31 @@ Let's show you some basic routes here.
8282

8383
Respond 'Hello World!' on the homepage or root:
8484

85-
```ps
85+
```powershell
8686
New-PolarisRoute -Method GET -Path "/" -ScriptBlock {
8787
$Response.Send('Hello World!')
8888
}
8989
```
9090

9191
New POST route at the root of the application:
9292

93-
```ps
93+
```powershell
9494
New-PolarisRoute -Method POST -Path "/" -ScriptBlock {
9595
$Respond.Send('Received POST request')
9696
}
9797
```
9898

9999
New PUT route to the /Process route:
100100

101-
```ps
101+
```powershell
102102
New-PolarisRoute -Method PUT -Path "/Process" -ScriptBlock {
103103
$Response.Send('Received a PUT request at the /Process route')
104104
}
105105
```
106106

107107
New DELETE route at the /Process route:
108108

109-
```ps
109+
```powershell
110110
New-PolarisRoute -Method DELETE -Path "/Process" -ScriptBlock {
111111
$Response.Send('Received a DELETE request at the /Process route')
112112
}
@@ -135,7 +135,7 @@ Let's say you have a simple static website that you want to serve using Polaris.
135135

136136
I can serve all of these files automatically using the following command:
137137

138-
```ps
138+
```powershell
139139
New-PolarisStaticRoute -RoutePath "/" -FolderPath "C:\MyAwesomeSite"
140140
```
141141

@@ -150,7 +150,7 @@ http://localhost:8080/favicon.ico
150150

151151
Polaris also has a built-in directory browser you can enable that can be used to set up a simple file server:
152152

153-
```ps
153+
```powershell
154154
New-PolarisStaticRoute -RoutePath "/" -FolderPath "C:\MyFolderOfFiles" -EnableDirectoryBrowser $True
155155
```
156156

@@ -160,7 +160,7 @@ APIs are all the rage today and working with them from a browser means you are g
160160

161161
Let's create a quick API for showing information about running processes:
162162

163-
```ps
163+
```powershell
164164
New-PolarisRoute -Method GET -ScriptBlock {
165165
$ProcessInfo = Get-Process | Select ProcessName,Id,CPU | ConvertTo-Json
166166
$Response.json($ProcessInfo)

docs/about_Polaris.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Polaris can be run on Windows or Linux or Mac. As long as you can install PowerS
2828

2929
A quick example of an API is the below command which will start Polaris listening on http://localhost:8080 for a GET request to the /helloworld path.
3030

31-
```ps
31+
```powershell
3232
Install-Module Polaris
3333
New-PolarisGetRoute -Path "/helloworld" -Scriptblock {
3434
$Response.Send('Hello World!')
@@ -41,7 +41,7 @@ I can get a response from the server by either opening a browser to http://local
4141

4242
**Command**
4343

44-
```ps
44+
```powershell
4545
PS> Invoke-RestMethod -Method GET -Uri http://localhost:8080/helloworld
4646
```
4747

lib/Polaris.Class.ps1

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -52,57 +52,61 @@ class Polaris {
5252

5353
# Run middleware in the order in which it was added
5454
foreach ($Middleware in $Polaris.RouteMiddleware) {
55-
$InformationVariable += $Polaris.InvokeRoute(
56-
$Middleware.Scriptblock,
57-
$Null,
58-
$Request,
59-
$Response
60-
)
61-
}
62-
63-
$Polaris.Log("Parsed Route: $Route")
64-
$Polaris.Log("Request Method: $($RawRequest.HttpMethod)")
65-
$Routes = $Polaris.ScriptblockRoutes
66-
67-
#
68-
# Searching for the first route that matches by the most specific route paths first.
69-
#
70-
$MatchingRoute = $Routes.keys | Sort-Object -Property Length -Descending | Where-Object { $Route -match [Polaris]::ConvertPathToRegex($_) } | Select-Object -First 1
71-
$Request.Parameters = ([PSCustomObject]$Matches)
72-
Write-Debug "Parameters: $Parameters"
73-
$MatchingMethod = $false
74-
75-
if ($MatchingRoute) {
76-
$MatchingMethod = $Routes[$MatchingRoute].keys -contains $Request.Method
77-
}
78-
79-
if ($MatchingRoute -and $MatchingMethod) {
80-
try {
81-
55+
if ($Response.Sent -eq $false) {
8256
$InformationVariable += $Polaris.InvokeRoute(
83-
$Routes[$MatchingRoute][$Request.Method],
84-
$Parameters,
57+
$Middleware.Scriptblock,
58+
$Null,
8559
$Request,
8660
$Response
8761
)
88-
89-
}
90-
catch {
91-
$ErrorsBody = ''
92-
$ErrorsBody += $_.Exception.ToString()
93-
$ErrorsBody += $_.InvocationInfo.PositionMessage + "`n`n"
94-
$Response.Send($ErrorsBody)
95-
$Polaris.Log($_)
96-
$Response.SetStatusCode(500)
9762
}
9863
}
99-
elseif ($MatchingRoute) {
100-
$Response.Send("Method not allowed")
101-
$Response.SetStatusCode(405)
102-
}
103-
else {
104-
$Response.Send("Not found")
105-
$Response.SetStatusCode(404)
64+
65+
if ($Response.Sent -eq $false) {
66+
$Polaris.Log("Parsed Route: $Route")
67+
$Polaris.Log("Request Method: $($RawRequest.HttpMethod)")
68+
$Routes = $Polaris.ScriptblockRoutes
69+
70+
#
71+
# Searching for the first route that matches by the most specific route paths first.
72+
#
73+
$MatchingRoute = $Routes.keys | Sort-Object -Property Length -Descending | Where-Object { $Route -match [Polaris]::ConvertPathToRegex($_) } | Select-Object -First 1
74+
$Request.Parameters = ([PSCustomObject]$Matches)
75+
Write-Debug "Parameters: $Parameters"
76+
$MatchingMethod = $false
77+
78+
if ($MatchingRoute) {
79+
$MatchingMethod = $Routes[$MatchingRoute].keys -contains $Request.Method
80+
}
81+
82+
if ($MatchingRoute -and $MatchingMethod) {
83+
try {
84+
85+
$InformationVariable += $Polaris.InvokeRoute(
86+
$Routes[$MatchingRoute][$Request.Method],
87+
$Parameters,
88+
$Request,
89+
$Response
90+
)
91+
92+
}
93+
catch {
94+
$ErrorsBody = ''
95+
$ErrorsBody += $_.Exception.ToString()
96+
$ErrorsBody += $_.InvocationInfo.PositionMessage + "`n`n"
97+
$Response.Send($ErrorsBody)
98+
$Polaris.Log($_)
99+
$Response.SetStatusCode(500)
100+
}
101+
}
102+
elseif ($MatchingRoute) {
103+
$Response.Send("Method not allowed")
104+
$Response.SetStatusCode(405)
105+
}
106+
else {
107+
$Response.Send("Not found")
108+
$Response.SetStatusCode(404)
109+
}
106110
}
107111

108112
# Handle logs
@@ -345,7 +349,9 @@ class Polaris {
345349
[System.Net.WebHeaderCollection]$Headers
346350
) {
347351
$RawResponse.StatusCode = $StatusCode
348-
$RawResponse.Headers = $Headers
352+
foreach ($Header in $Headers.Keys) {
353+
$RawResponse.AddHeader($Header, $Headers.Get($Header))
354+
}
349355
if ($ByteResponse.Length -gt 0) {
350356
$RawResponse.ContentType = $ContentType
351357
}

0 commit comments

Comments
 (0)