Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for enabling sessions for Tabs #1242

Merged
merged 3 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 59 additions & 15 deletions docs/Tutorials/Middleware/Types/Sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@

Session Middleware is supported on web requests and responses in the form of signed a cookie/header and server-side data storage. When configured the middleware will check for a session cookie/header (usually called `pode.sid`) on the request; if a cookie/header is not found on the request, or the session is not in storage, then a new session is created and attached to the response. If there is a session, then the appropriate data for that session is loaded from storage.

The duration of the session cookie/header can be specified, as well as whether to extend the duration each time on each request. A secret-key to sign sessions can be supplied (default is a random GUID), as well as the ability to specify custom data stores - the default is in-memory, but custom storage could be anything like Redis/MongoDB/etc.
The duration of the session cookie/header can be specified, as well as whether to extend the duration each time on each request. A secret key to sign sessions can be supplied (default is a random GUID), as well as the ability to specify custom data stores - the default is in-memory, but custom storage could be anything like Redis/MongoDB/etc.

!!! note
Using sessions via headers is best used with REST APIs and the CLI. It's not advised to use them for normal websites, as browsers don't send back response headers in new requests - unlike cookies.

!!! tip
Sessions are typically used in conjunction with Authentication, but can you use them standalone as well!
Sessions are typically used in conjunction with Authentication, but you can use them standalone as well!

## Usage

To initialise sessions in Pode you'll need to call [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware). This function will configure and automatically create the Middleware needed to enable sessions. By default sessions are set to use cookies, but support is also available for headers.
To initialise sessions in Pode you'll need to call [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware). This function will configure and automatically create the Middleware needed to enable sessions. By default, sessions are set to use cookies, but support is also available for headers.

Sessions are automatically signed using a random GUID. For Pode running on a single server using the default in-memory storage this is OK, however if you're running Pode on multiple servers, or if you're defining a custom storage then a `-Secret` is required - this is so that sessions from different servers, or after a server restart, don't become corrupt and unusable.

### Cookies

The following is an example of how to setup session middleware using cookies. The duration of each session is defined as a total number of seconds via the `-Duration` parameter; here we set the duration to 120, so each session created will expire after 2mins, but the expiry time will be extended each time the session is used:
The following is an example of how to set up session middleware using cookies. The duration of each session is defined as a total number of seconds via the `-Duration` parameter; here we set the duration to 120, so each session created will expire after 2mins, but the expiry time will be extended each time the session is used:

```powershell
Start-PodeServer {
Expand All @@ -30,7 +30,7 @@ The default name of the session cookie is `pode.sid`, but this can be customised

### Headers

Sessions are also supported using headers - useful for CLI requests. The following example will enable sessions use headers instead of cookies, and will also set each session created to have a `-Duration` of 120 seconds:
Sessions are also supported using headers - useful for CLI requests. The following example will enable sessions to use headers instead of cookies, and will also set each session created to have a `-Duration` of 120 seconds:

```powershell
Start-PodeServer {
Expand All @@ -40,13 +40,54 @@ Start-PodeServer {

When using headers, the default name of the session header in the request/response is `pode.sid` - this can be customised using the `-Name` parameter. When you make an initial request to authenticate some user, the `pode.sid` header will be returned in the response. You can then use the value of this header in subsequent requests for the authenticated user, and then make a call using the session one last time against some route to expire the session - or just let it automatically expire.

## Scope

Sessions have two different Scopes: Browser and Tab. You can specify the scope using the `-Scope` parameter on [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware).

!!! important
Using the Tabs scope requires additional frontend logic, and doesn't work out-of-the-box like the Browser scope. See more [below](#tabs).

The default Scope is Browser, where any authentication and general session data is shared across all tabs in a browser. If you have a site with a view counter and you log in to your site on one tab, and then open another tab and navigate to your site, you'll be automatically logged in and see the same view counter results in both tabs.

The Tabs scope still shares authentication data, so if you log in to your site in one tab and open it again in a different tab, you'll still be logged in. However, general data is separated; taking the view counter example above, both tabs will show different results for the view counter.

### Tabs

Unlike the Browser scope which works out-of-the-box when enabled, the Tabs scope requires additional frontend logic.

For session data to be split across different tabs, you need to inform Pode about what the "TabId" is for each tab. This is done by supplying an `X-PODE-SESSION-TAB-ID` HTTP header in the request. From a browser on a normal page request, there's no way to supply this header, and the normal base session will be supplied instead - hence why authentication data remains shared across tabs. However, if you load the content of your site asynchronously, or via any other means, you can supply this header and it will let you split general session data across tabs.

In websites, the TabId is typically generated via JavaScript, and stored in `window.sessionStorage`. However, you have to be careful with this approach, as it does make tab sessions susceptible to XSS attacks. The following is a similar approach as used by Pode.Web:

```javascript
// set TabId
if (window.sessionStorage.TabId) {
window.TabId = window.sessionStorage.TabId;
window.sessionStorage.removeItem("TabId");
}
else {
window.TabId = Math.floor(Math.random() * 1000000);
}

// binding to persist TabId on refresh
window.addEventListener("beforeunload", function(e) {
window.sessionStorage.TabId = window.TabId;
return null;
});
```

The TabId could then be sent as an HTTP header using AJAX. There are other approaches available online as well.

## SessionIds

The inbuilt SessionId generator used for sessions is a GUID, but you can supply a custom generator using the `-Generator` parameter.
The built-in SessionId generator used for sessions is a GUID, but you can supply a custom generator using the `-Generator` parameter.

If supplied, the `-Generator` is a scriptblock that must return a valid string. The string itself should be a random unique value, that can be used as a unique session identifier.

Within a route, or middleware, you can get the currently authenticated session'd ID using [`Get-PodeSessionId`](../../../../Functions/Sessions/Get-PodeSessionId). If there is no session, or the session is not authenticated, then `$null` is returned. This function can also returned the fully signed sessionId as well. If you want the sessionId even if it's not authenticated, then you can supply `-Force` to get the current SessionId back.
Within a route, or middleware, you can get the currently authenticated session's ID using [`Get-PodeSessionId`](../../../../Functions/Sessions/Get-PodeSessionId). If there is no session, or the session is not authenticated, then `$null` is returned. This function can also return the fully signed sessionId as well. If you want the sessionId even if it's not authenticated, then you can supply `-Force` to get the current SessionId back.

!!! note
If you're using the Tab `-Scope`, then the SessionId will include the TabId as well, if one was supplied.

### Strict

Expand All @@ -66,7 +107,7 @@ You can define a custom storage by supplying a `psobject` to the `-Storage` para
[void] Delete([string] $sessionId)
```

For example, the following is a mock up of a Storage for Redis (note that the functions are fake):
For example, the following is a mock-up of a Storage for Redis (note that the functions are fake):

```powershell
# create the object
Expand Down Expand Up @@ -97,15 +138,15 @@ Enable-PodeSessionMiddleware -Duration 120 -Storage $store -Secret 'schwifty'

## Session Data

To add data to a session you can utilise the `.Session.Data` property within the [web event](../../../WebEvent) object accessible in a Route - or other Middleware. The data will be saved to some storage at the end of the route automatically using Endware. When a request is made using the same SessionId, the data is loaded from the store. For example, incrementing some view counter:
To add data to a session you can use the `.Session.Data` property within the [web event](../../../WebEvent) object, which is accessible in a Route or other Middleware. The data will be saved to some storage at the end of the request automatically using Endware. When a request is made using the same SessionId, the data is loaded from the store. For example, incrementing some view counter:

```powershell
Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
$WebEvent.Session.Data.Views++
}
```

You can also use the `$session:` variable scope, which will get/set data on the current session for the name supplied. You can use `$session:` anywhere a `$WebEvent` is available - such as Routes, Middleware, Authentication and Endware. The same view counter example above would now be as follows:
You can also use the `$session:` variable scope, which will get/set data on the current session for the name supplied. You can use `$session:` anywhere a `$WebEvent` is available - such as Routes, Middleware, Authentication, and Endware. The same view counter example above would now be as follows:

```powershell
Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
Expand All @@ -116,19 +157,22 @@ Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
A session's data will be automatically saved by Pode at the end of each request, but you can force the data of the current session to be saved by using [`Save-PodeSession`](../../../../Functions/Sessions/Save-PodeSession).

!!! important
`$session:` can only be used in the main scriptblocks of Routes, etc. If you attempt to use it in a function of a custom module, it will fail; even if you're using the function in a route. Pode remaps `$session:` on server start, and can only do this to the main scriptblocks supplied to functions such as `Add-PodeRoute`. In these scenarios you will have to use `$WebEvent.Session.Data`.
`$session:` can only be used in the main scriptblocks of Routes, etc. If you attempt to use it in a function of a custom module, it will fail; even if you're using the function in a route. Pode remaps `$session:` on server start, and can only do this to the main scriptblocks supplied to functions such as `Add-PodeRoute`. In these scenarios, you will have to use `$WebEvent.Session.Data`.

!!! note
If using the Tab `-Scope`, any session data will be stored separately from other tabs. This allows you to have multiple tabs open for the same site/page but all with separate session data. Any authentication data will still be shared.

## Expiry

When you enable Sessions using [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware) you can define the duration of each session created, in seconds, using the `-Duration` parameter. When a session is created its expiry is set to `DateTime.UtcNow + Duration`, and by default a session will automatically expire when the calculated DateTime is reached:
When you enable Sessions using [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware) you can define the duration of each session created, in seconds, using the `-Duration` parameter. When a session is created its expiry is set to `DateTime.UtcNow + Duration`, and by default, a session will automatically expire when the calculated DateTime is reached:

```powershell
Start-PodeServer {
Enable-PodeSessionMiddleware -Duration 120
}
```

You can tell Pode to reset/extend each session's expiry on each request sent, that uses that SessionId, by passing the `-Extend` switch. When a session's expiry is reset/extended, the DateTime/Duration calculation is re-calculated:
You can tell Pode to reset/extend each session's expiry on each request sent, that uses that SessionId, by supplying the `-Extend` switch. When a session's expiry is reset/extended, the DateTime/Duration calculation is re-calculated:

```powershell
Start-PodeServer {
Expand All @@ -138,7 +182,7 @@ Start-PodeServer {

### Retrieve

You can retrieve the expiry for the current session by using [`Get-PodeSessionExpiry`](../../../../Functions/Sessions/Get-PodeSessionExpiry). If you use this function without `-Extend` specified originally then this will return the explicit DateTime the current session will expire. However, if you did setup sessions to extend the this function will return the recalculated expiry for the current session on each call:
You can retrieve the expiry for the current session by using [`Get-PodeSessionExpiry`](../../../../Functions/Sessions/Get-PodeSessionExpiry). If you use this function without `-Extend` specified originally then this will return the explicit DateTime the current session will expire. However, if you did set up sessions to extend then this function will return the recalculated expiry for the current session on each call:

```powershell
Start-PodeServer {
Expand Down Expand Up @@ -168,7 +212,7 @@ Start-PodeServer {

### Reset

For any session created when `-Extend` wasn't supplied to [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware) will always have a explicit DateTime set for expiring. However, you can reset this expiry date using [`Reset-PodeSessionExpiry`](../../../../Functions/Sessions/Reset-PodeSessionExpiry), and the current session's expiry will be recalculated from now plus the specifed `-Duration`:
For any session created when `-Extend` wasn't supplied to [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware) will always have an explicit DateTime set for expiring. However, you can reset this expiry date using [`Reset-PodeSessionExpiry`](../../../../Functions/Sessions/Reset-PodeSessionExpiry), and the current session's expiry will be recalculated from now plus the specified `-Duration`:

```powershell
Start-PodeServer {
Expand Down
6 changes: 3 additions & 3 deletions examples/web-auth-form.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Start-PodeServer -Threads 2 {
if ($username -eq 'morty' -and $password -eq 'pickle') {
return @{
User = @{
ID ='M0R7Y302'
ID = 'M0R7Y302'
Name = 'Morty'
Type = 'Human'
}
Expand All @@ -55,8 +55,8 @@ Start-PodeServer -Threads 2 {

Write-PodeViewResponse -Path 'auth-home' -Data @{
Username = $WebEvent.Auth.User.Name
Views = $session:Views
Expiry = Get-PodeSessionExpiry
Views = $session:Views
Expiry = Get-PodeSessionExpiry
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/Pode.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@
'Reset-PodeSessionExpiry',
'Get-PodeSessionDuration',
'Get-PodeSessionExpiry',
'Test-PodeSessionsEnabled',
'Get-PodeSessionTabId',
'Get-PodeSessionInfo',
'Test-PodeSessionScopeIsBrowser',

# auth
'New-PodeAuthScheme',
Expand Down
Loading
Loading