-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSessionPolicy.php
More file actions
166 lines (143 loc) · 5.51 KB
/
Copy pathSessionPolicy.php
File metadata and controls
166 lines (143 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/**
* Italix Session - SessionPolicy
*
* @package Italix\Session
*/
declare(strict_types=1);
namespace Italix\Session;
/**
* How long a session lives, in the three senses that are actually different.
*
* PHP has one number for this (`session.gc_maxlifetime`, 1440 seconds by
* default) and it is an idle window enforced by a probabilistic sweep. Three
* separate limits are what a real policy needs:
*
* - **idle** — untouched for this long, and it is over. The usual one.
* - **absolute** — this old, however busy. Mandated in clinical and public
* sector work, and the only cap a stolen cookie cannot outlive.
* - **rotation grace** — how long a just-rotated identifier is still followed
* to its successor instead of being reported as a replay.
*
* Plus one limit that is not about time at all: **how many sessions one subject
* may hold at once**. It lives here because it is the same kind of thing — a
* rule about the shape of a session's life, decided once in configuration — and
* because the alternative is every application reimplementing it in its login
* controller. Off by default: see `max_concurrent()` for why that is not
* timidity.
*
* Nothing here is enforced by a sweep: expiry is decided on read, so a session
* is over the moment it is over and no cron job is load-bearing. `gc()` exists
* on the stores to keep tables small, which is housekeeping, not correctness.
*/
final class SessionPolicy
{
/** Two hours. Long enough for a form, short enough for a shared desk. */
public const DEFAULT_IDLE_TTL_N = 7200;
/** One minute, which is longer than any legitimate in-flight request set. */
public const DEFAULT_ROTATION_GRACE_N = 60;
private int $idle_ttl_n = self::DEFAULT_IDLE_TTL_N;
/** 0 means no absolute cap — the default, because most applications do not want one. */
private int $absolute_ttl_n = 0;
private int $rotation_grace_n = self::DEFAULT_ROTATION_GRACE_N;
/**
* 0 means no limit, and that is the default on purpose.
*
* A cap on simultaneous sessions is a **product** decision, not a security
* one: it is how seat-based pricing stops one login being shared by an
* office, and plenty of applications have no business having it. A library
* that shipped it switched on would be making a pricing decision for
* everybody who adopted it.
*/
private int $max_concurrent_n = 0;
/**
* Seconds of inactivity after which the session is `EXPIRED_IDLE`.
* Zero or less disables the idle window entirely.
*/
public function idle_ttl(int $ttl_n): self
{
$this->idle_ttl_n = $ttl_n;
return $this;
}
/**
* Seconds since creation after which the session is `EXPIRED_ABSOLUTE`,
* regardless of activity. Zero means no cap.
*/
public function absolute_ttl(int $ttl_n): self
{
$this->absolute_ttl_n = $ttl_n;
return $this;
}
/**
* How long a rotated identifier still resolves to its successor.
*
* Set this to zero and a browser that had two requests in flight during a
* login gets a replay alarm and a mass revocation, which is a self-inflicted
* outage. It is a race window, not a security parameter — keep it small and
* non-zero.
*/
public function rotation_grace(int $grace_n): self
{
$this->rotation_grace_n = max(0, $grace_n);
return $this;
}
/**
* At most this many live sessions per subject; the oldest are signed out to
* make room. Zero, the default, means no limit.
*
* **Evicting rather than refusing** is the deliberate half. A cap that
* refuses the new login locks somebody out of their own account because of a
* browser they closed on a machine they no longer have — support tickets, and
* a user who cannot fix it themselves. Evicting the least recently used
* session is what every consumer service that does this settled on, and the
* person is present to notice.
*
* An application that genuinely wants to refuse asks first, and then it owns
* the message:
*
* if (count($sessions->of_subject($subject_c)) >= $limit_n) { … }
*
* **This needs a store that can index.** `Session::begin()` refuses a policy
* carrying a cap over a store whose `can_find_by_subject()` is false, rather
* than accepting a limit it would then silently not apply — the same
* treatment `CookieSpec` gives a description a browser would discard.
*/
public function max_concurrent(int $sessions_n): self
{
$this->max_concurrent_n = max(0, $sessions_n);
return $this;
}
public function get_idle_ttl_n(): int
{
return $this->idle_ttl_n;
}
public function get_absolute_ttl_n(): int
{
return $this->absolute_ttl_n;
}
public function get_rotation_grace_n(): int
{
return $this->rotation_grace_n;
}
public function has_idle_window(): bool
{
return $this->idle_ttl_n > 0;
}
public function has_absolute_cap(): bool
{
return $this->absolute_ttl_n > 0;
}
public function get_max_concurrent_n(): int
{
return $this->max_concurrent_n;
}
public function has_concurrency_cap(): bool
{
return $this->max_concurrent_n > 0;
}
}