security

object

%audit

/phlo/resources/security/audit.phlo
version 1.0
creator q-ai.nl
summary Audit log for model mutations (opt-in via static idColumn/objAudit). Schema: resources/security/audit.sql
package security
frontend false
backend true
tags audit log compliance traceability
static

audit :: log ($model, $action, $before = [], $after = [], $exclude = [])

line 9
Logt wijzigingen die aan een model zijn aangebracht in het auditlogboek, waarbij details zoals de gebruiker, het type actie en de wijzigingen voor en na de aanpassing worden vastgelegd.
$class = is_object($model) ? get_class($model) : (string)$model
$pk = (is_string($class) && class_exists($class) && property_exists($class, 'idColumn')) ? $class::$idColumn : 'id'
$id = is_object($model) ? ($model->$pk ?? $model->id ?? null) : null
if ($id === null) return
$before = (array)$before
$after = (array)$after
foreach ($exclude AS $col) unset($before[$col], $after[$col])
$changes = $action === 'update' ? static::diff($before, $after) : ($action === 'create' ? $after : $before)
$class::DB()->query(
	'INSERT INTO audit_log (ts, '.$class::DB()->quoteId('user').', model, record_id, action, changes, ip) VALUES (?, ?, ?, ?, ?, ?, ?)',
	time(),
	class_exists('session') && isset(%session->user) ? (int)%session->user : null,
	$class,
	(string)$id,
	$action,
	json_encode($changes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
	(string)($_SERVER['REMOTE_ADDR'] ?? null),
)
static

audit :: diff ($before, $after)

line 30
Berechnet de verschillen tussen twee datasets en retourneert een array van gewijzigde eigenschappen met hun oude en nieuwe waarden.
$changed = []
foreach ($after AS $col => $newVal){
	$oldVal = $before[$col] ?? null
	if ($oldVal !== $newVal) $changed[$col] = ['from' => $oldVal, 'to' => $newVal]
}
return $changed
static

audit :: history ($model, $recordId, $limit = 50)

line 39
Haal de auditgeschiedenis op voor een specifiek model en record-ID uit de audit_log-tabel, gesorteerd op tijdstempel in aflopende volgorde, met een limiet op het aantal resultaten.
$class = is_object($model) ? get_class($model) : (string)$model
return $class::DB()->query(
	'SELECT * FROM audit_log WHERE model=? AND record_id=? ORDER BY ts DESC LIMIT ?',
	$class, (string)$recordId, (int)$limit,
)->fetchAll(\PDO::FETCH_OBJ)
static

audit :: byUser ($model, $userId, $fromTs = 0, $limit = 100)

line 47
Haal auditlogboekvermeldingen op voor een specifieke gebruiker, gefilterd op een tijdstempel en beperkt tot een bepaald aantal resultaten.
return $model::DB()->query(
	'SELECT * FROM audit_log WHERE '.$model::DB()->quoteId('user').'=? AND ts >= ? ORDER BY ts DESC LIMIT ?',
	(int)$userId, (int)$fromTs, (int)$limit,
)->fetchAll(\PDO::FETCH_OBJ)
static

audit :: purge ($model, $olderThanSeconds = 31536000)

line 54
Verwijdert vermeldingen uit de audit_log-tabel die ouder zijn dan een opgegeven aantal seconden.
$model::DB()->query('DELETE FROM audit_log WHERE ts < ?', time() - $olderThanSeconds)
object

%captcha

/phlo/resources/security/captcha.phlo
version 1.0
creator q-ai.nl
summary Self-contained interactive slider-puzzle captcha (no external service). The server picks a secret gap position and renders the background plus a loose piece with GD; the client drags the piece into place. verify() checks the end position plus human drag behaviour (time, path, variation). Single-use and session-bound; the gap position never leaves the server.
package security
frontend true
backend true
requires @session lang DOM/exists php-ext:gd
tags captcha spam bot human-verification security
advice verify() does not consume the captcha; call consume() only on success.
static

captcha :: W

line 10
Genereert een CAPTCHA-afbeelding met een breedte van 300 pixels.
300
static

captcha :: H

line 11
Deze constante vertegenwoordigt de hoogte van de CAPTCHA-afbeelding in pixels.
180
static

captcha :: P

line 12
captcha::$P is een constante die de waarde van de captcha-parameter bevat die wordt gebruikt voor validatie in Phlo-toepassingen.
56
static

captcha :: tol

line 13
captcha::$tol retourneert het tolerantieniveau voor het CAPTCHA-validatieproces, dat bepaalt hoe soepel het systeem is bij het beoordelen van gebruikersreacties.
8
static

captcha :: ttl

line 14
Stelt de time-to-live (TTL) voor de captcha in seconden in.
600
static

captcha :: issue

line 16
Genereert een willekeurige kloof voor een captcha-uitdaging en slaat de kloofwaarden op in de sessie met een vervaltijd.
$gapX = random_int(static::$P + 14, static::$W - static::$P - 6)
$gapY = random_int(12, static::$H - static::$P - 12)
%session->captcha = arr(gap: $gapX, exp: time() + static::$ttl)
return arr(gapX: $gapX, gapY: $gapY)
static

captcha :: images ($gapX, $gapY)

line 23
Genereert een CAPTCHA-afbeelding en een puzzelstuk, en retourneert deze als base64-gecodeerde PNG-gegevens. De achtergrondafbeelding heeft een verloop en willekeurige ellipsen, terwijl het stuk een bijgesneden sectie van de achtergrond is.
$w = static::$W
$h = static::$H
$p = static::$P
$bg = imagecreatetruecolor($w, $h)
$r0 = random_int(50, 170)
$g0 = random_int(50, 170)
$b0 = random_int(50, 170)
for ($x = 0; $x < $w; $x++){
	$col = imagecolorallocate($bg, (int)($r0 + $x * 0.5) % 256, (int)($g0 + $x * 0.2 + 30) % 256, (int)($b0 + ($w - $x) * 0.4) % 256)
	imagefilledrectangle($bg, $x, 0, $x, $h, $col)
}
imagealphablending($bg, true)
for ($i = 0; $i < 7; $i++){
	$col = imagecolorallocatealpha($bg, random_int(0, 255), random_int(0, 255), random_int(0, 255), random_int(55, 95))
	imagefilledellipse($bg, random_int(0, $w), random_int(0, $h), random_int(50, 130), random_int(50, 130), $col)
}
$piece = imagecreatetruecolor($p, $p)
imagecopy($piece, $bg, 0, 0, $gapX, $gapY, $p, $p)
imagerectangle($piece, 0, 0, $p - 1, $p - 1, imagecolorallocate($piece, 245, 245, 245))
imagefilledrectangle($bg, $gapX, $gapY, $gapX + $p - 1, $gapY + $p - 1, imagecolorallocatealpha($bg, 0, 0, 0, 95))
imagerectangle($bg, $gapX, $gapY, $gapX + $p - 1, $gapY + $p - 1, imagecolorallocate($bg, 255, 255, 255))
ob_start()
imagepng($bg)
$bgData = base64_encode(ob_get_clean())
ob_start()
imagepng($piece)
$pieceData = base64_encode(ob_get_clean())
return arr(bg: 'data:image/png;base64,'.$bgData, piece: 'data:image/png;base64,'.$pieceData)
static

captcha :: verify ($x, $telemetry)

line 55
Verifieert een captcha door sessiegegevens, telemetrie en verschillende voorwaarden te controleren om de geldigheid van de reactie te waarborgen.
$c = %session->captcha ?? null
if (!$c || time() > (int)($c['exp'] ?? 0)) return false
if (abs((int)round((float)$x) - (int)($c['gap'] ?? -999)) > static::$tol) return false
$t = json_decode((string)$telemetry, true)
if (!is_array($t)) return false
$d = (int)($t['d'] ?? 0)
$n = (int)($t['n'] ?? 0)
$xs = is_array($t['x'] ?? null) ? array_values($t['x']) : []
if ($d < 200 || $d > 60000) return false
if ($n < 5 || count($xs) < 4) return false
$deltas = []
for ($i = 1; $i < count($xs); $i++) $deltas[] = (float)$xs[$i] - (float)$xs[$i - 1]
if (array_sum(array_map('abs', $deltas)) < 30) return false
$mean = array_sum($deltas) / count($deltas)
$var = 0
foreach ($deltas AS $dd) $var += ($dd - $mean) ** 2
if (sqrt($var / count($deltas)) < 0.5) return false
return true
static

captcha :: consume

line 76
Verwijdert de captcha-gegevens uit de sessie, waardoor deze effectief wordt geconsumeerd.
unset(%session->captcha)
method

%captcha -> widget

line 80
Genereert een CAPTCHA-widget door een uitdaging uit te geven en een afbeelding te maken met gespecificeerde parameters.
$ch = static::issue()
$img = static::images($ch['gapX'], $ch['gapY'])
return (string)$this->field($img['bg'], $img['piece'], $ch['gapY'], static::$W, static::$H, static::$P)
view

%captcha -> field ($bg, $piece, $gapY, $w, $h, $p)

line 86
Genereert een CAPTCHA-weergave die een slepen-schuifknop bevat waarmee gebruikers een puzzel kunnen voltooien, wat de beveiliging tegen geautomatiseerde inzendingen verbetert.
<div#captcha data-w="$w" data-h="$h" data-p="$p" data-gapy="$gapY">
	<div.stage>
		<img.bg src="{{ $bg }}" alt="">
		<img.piece src="{{ $piece }}" alt="" draggable="false">
	</div>
	<div.track>
		<div.fill></div>
		<button.thumb type="button" tabindex="-1" aria-label="{{ en('Drag the slider until the piece fits') }}">&#8250;&#8250;</button>
	</div>
	<p.hint>{en: Drag the slider until the piece snaps into place}</p>
	<input type=hidden name=captcha_x>
	<input type=hidden name=captcha_t>
</div>
view

style

line 101
Definieert de CSS-stijlen voor de captcha-component, inclusief lay-out, kleuren en interactieve elementen.
#captcha {
	margin: 14px 0 4px
	max-width: 300px
}
#captcha .stage {
	border-radius: 8px
	line-height: 0
	overflow: hidden
	position: relative
	touch-action: none
	user-select: none
	width: 100%
}
#captcha .bg {
	display: block
	height: auto
	pointer-events: none
	width: 100%
}
#captcha .piece {
	box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25), 0 2px 6px rgba(0, 0, 0, 0.4)
	left: 6px
	pointer-events: none
	position: absolute
	top: 0
}
#captcha .track {
	background: #e7e5e4
	border-radius: 8px
	height: 42px
	margin-top: 10px
	position: relative
	touch-action: none
}
#captcha .fill {
	background: #fed7aa
	border-radius: 8px
	height: 100%
	left: 0
	position: absolute
	top: 0
	width: 0
}
#captcha .thumb {
	align-items: center
	appearance: none
	background: #ea580c
	border: 0
	border-radius: 8px
	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3)
	color: #fff
	cursor: grab
	display: flex
	font-family: inherit
	font-size: 18px
	font-weight: 700
	height: 42px
	justify-content: center
	left: 0
	margin: 0
	padding: 0
	position: absolute
	top: 0
	touch-action: none
	width: 42px
}
#captcha .thumb:active: cursor: grabbing
#captcha .thumb:hover: background: #c2410c
#captcha .hint {
	color: #78716c
	font-size: 12px
	margin: 6px 0 0
}
view

script

line 177
const captcha = {
	scale: 1, startCss: 6, travelMax: 0, maxThumb: 0,
	dragging: false, t0: 0, samples: [],
	cap: null, bg: null, piece: null, thumb: null, track: null, fill: null, xField: null, tField: null, submit: null,
	grab(){
		this.cap = obj('#captcha')
		if (!this.cap) return false
		this.bg = obj('.bg', this.cap)
		this.piece = obj('.piece', this.cap)
		this.thumb = obj('.thumb', this.cap)
		this.track = obj('.track', this.cap)
		this.fill = obj('.fill', this.cap)
		this.xField = obj('[name=captcha_x]', this.cap)
		this.tField = obj('[name=captcha_t]', this.cap)
		this.submit = obj('#registerSubmit')
		return true
	},
	layout(){
		if (!this.grab()) return
		const Wr = this.bg.clientWidth || +this.cap.dataset.w
		this.scale = Wr / +this.cap.dataset.w
		const pieceCss = +this.cap.dataset.p * this.scale
		this.piece.style.width = pieceCss + 'px'
		this.piece.style.height = pieceCss + 'px'
		this.piece.style.top = (+this.cap.dataset.gapy * this.scale) + 'px'
		this.startCss = 6 * this.scale
		this.piece.style.left = this.startCss + 'px'
		this.travelMax = Wr - pieceCss - this.startCss
		this.maxThumb = this.track.clientWidth - this.thumb.clientWidth
		if (this.submit) this.submit.disabled = true
	},
	set(tx){
		tx = Math.max(0, Math.min(this.maxThumb, tx))
		this.thumb.style.left = tx + 'px'
		this.fill.style.width = (tx + this.thumb.clientWidth) + 'px'
		const frac = this.maxThumb > 0 ? tx / this.maxThumb : 0
		const pl = this.startCss + frac * (this.travelMax - this.startCss)
		this.piece.style.left = pl + 'px'
		return pl / this.scale
	},
	down(e){
		if (!this.grab()) return
		this.dragging = true
		this.t0 = performance.now()
		this.samples = []
		e.preventDefault()
	},
	move(e){
		if (!this.dragging) return
		const px = e.touches ? e.touches[0].clientX : e.clientX
		const rect = this.track.getBoundingClientRect()
		const nx = this.set(px - rect.left - this.thumb.clientWidth / 2)
		this.samples.push({x: nx, t: performance.now() - this.t0})
	},
	up(){
		if (!this.dragging) return
		this.dragging = false
		const xs = this.samples.map(s => Math.round(s.x))
		this.xField.value = xs.length ? xs[xs.length - 1] : 0
		const step = Math.max(1, Math.ceil(xs.length / 24))
		this.tField.value = JSON.stringify({d: Math.round(this.samples.length ? this.samples[this.samples.length - 1].t : 0), n: this.samples.length, x: xs.filter((_, i) => i % step === 0)})
		if (this.submit) this.submit.disabled = false
	}
}

onExist('#captcha', () => captcha.layout())
on('load', '#captcha .bg', () => captcha.layout())
on('resize', window, () => captcha.layout())
on('mousedown touchstart', '#captcha .thumb', (el, e) => captcha.down(e))
on('mousemove touchmove', window, (el, e) => last(captcha.move(e), false))
on('mouseup touchend', window, (el, e) => captcha.up())
object

%creds

/phlo/resources/security/creds.phlo
version 1.0
creator q-ai.nl
summary Credentials resolver from env and ini sources
package security
frontend false
backend true
tags credentials env ini secrets configuration
method

%creds -> __construct (?array $values = null)

line 9
Initialiseert het creds-object, waarbij waarden worden opgelost als deze niet zijn opgegeven, en wijst elke waarde toe aan de bijbehorende eigenschap, waarbij indien nodig een nieuwe instantie van static of SensitiveParameterValue wordt gemaakt.
$values ??= $this->resolve()
foreach ($values AS $key => $value){
	$this->$key = is_array($value) ? new static($value) : new \SensitiveParameterValue((string)$value)
}
method

%creds -> resolve

line 16
Deze functie lost en voegt inloggegevens samen vanuit een INI-bestand en omgevingsvariabelen, en retourneert de gecombineerde gegevensarray.
$data = []
$this->merge($data, $this->loadINI(data.'creds.ini'))
$this->merge($data, $this->envValues(false))
$this->merge($data, $this->envValues(true))
return $data
method

%creds -> loadINI (string $file):array

line 24
Laadt configuratie-instellingen uit een INI-bestand dat is opgegeven door het gegeven bestandspad en retourneert deze als een associatieve array. Als het bestand niet bestaat of niet kan worden geparsed, retourneert het een lege array.
if (!is_file($file)) return []
$ini = parse_ini_file($file, true, INI_SCANNER_RAW)
return is_array($ini) ? $ini : []
method

%creds -> envValues (bool $hostScoped = false):array

line 30
Haalt omgevingsvariabele waarden op die beginnen met een opgegeven voorvoegsel, optioneel beperkt tot de host, en retourneert deze als een associatieve array.
$out = []
$prefix = $hostScoped ? ('PHLO_'.$this->hostKey().'__') : 'PHLO__'
$sources = []
is_array($_ENV ?? null) && $sources[] = $_ENV
is_array($_SERVER ?? null) && $sources[] = $_SERVER
is_array($env = getenv()) && $sources[] = $env
foreach ($sources AS $source){
	foreach ($source AS $key => $value){
		$key = (string)$key
		if (!str_starts_with($key, $prefix)) continue
		$path = substr($key, strlen($prefix))
		if (!$path) continue
		$this->envAssign($out, explode('__', $path), (string)$value)
	}
}
return $out
method

%creds -> hostKey

line 49
Deze functie verwerkt de host van de aanvraag, zet deze om naar hoofdletters, verwijdert niet-alfanumerieke tekens en trimt eventuele leidende of volgende scheidingstekens.
$host = strtoupper(%req->host)
$host = preg_replace('/[^A-Z0-9]+/', us, $host)
return trim($host, us)
method

%creds -> envAssign (array &$target, array $parts, string $value):void

line 55
Wijst een waarde toe aan een geneste arraystructuur op basis van de opgegeven delen, waarbij indien nodig tussenliggende arrays worden aangemaakt.
$parts = array_values(array_filter(loop($parts, fn($part) => trim($part)), 'strlen'))
if (!$parts) return
$node = &$target
$last = count($parts) - 1
foreach ($parts AS $i => $part){
	if ($i === $last){
		$node[$part] = $value
		return
	}
	if (!isset($node[$part]) || !is_array($node[$part])) $node[$part] = []
	$node = &$node[$part]
}
method

%creds -> merge (array &$base, array $add):void

line 70
Voegt de inhoud van de $add-array samen met de $base-array, waarbij waarden recursief worden gecombineerd als beide arrays zijn.
foreach ($add AS $key => $value){
	if (isset($base[$key]) && is_array($base[$key]) && is_array($value)){
		$this->merge($base[$key], $value)
		continue
	}
	$base[$key] = $value
}
method

%creds -> objGet ($key)

line 80
Haal de waarde op die aan de opgegeven sleutel is gekoppeld aan het object, en retourneer gevoelige waarden op een veilige manier.
if ($key === 'toArray') return loop($this->objData, fn($value) => is_a($value, 'SensitiveParameterValue') ? $value->getValue() : $value)
if (isset($this->objData[$key]) && is_a($this->objData[$key], '\SensitiveParameterValue')) return $this->objData[$key]->getValue()
method

%creds -> objInfo

line 85
Deze functie verwerkt elk item in objData, vervangt instanties van SensitiveParameterValue door sterren die overeenkomen met hun lengte, terwijl andere waarden ongewijzigd blijven.
loop($this->objData, fn($value) => is_a($value, '\SensitiveParameterValue') ? str_repeat('*', strlen($value->getValue())) : $value)
object

%CSRF

/phlo/resources/security/CSRF.phlo
version 1.0
creator q-ai.nl
summary Rotating async CSRF protection for Phlo requests
package security
frontend true
backend true
requires @session token payload
provides app.mod.csrf
tags csrf security async forms
view

%CSRF -> view

line 11
Genereert een meta-tag met de CSRF-token voor veilige formulierindieningen.
<meta name=csrf content="$this->token">
prop

%CSRF -> token

line 12
Genereert een CSRF-token van 32 tekens als er nog geen in de sessie bestaat.
%session->csrf ??= token(32)
method

%CSRF -> verify

line 13
Verifieert het CSRF-token door het te vergelijken met het token dat in de HTTP-header is ontvangen.
hash_equals($this->token, (string)($_SERVER['HTTP_X_CSRF_TOKEN'] ?? void))
method

%CSRF -> update

line 14
Werk de CSRF-token in de sessie bij en wijs deze toe aan de csrf-eigenschap.
arr(csrf: $this->token = %session->csrf = token(32))
view

script

line 16
Stelt de CSRF-tokenwaarde in de meta-tag van het document in.
app.mod.csrf = value => obj('meta[name="csrf"]').content = value
object

%JWT

/phlo/resources/security/JWT.phlo
version 1.0
creator q-ai.nl
summary Sign and verify compact HS256 JSON Web Tokens (RFC 7519), secure by default
package security
frontend false
backend true
requires php-ext:hash
tags jwt jws hs256 token auth security
method

%JWT -> __construct (public string $secret, public string $issuer = void, public int $leeway = 30)

line 10
Maakt een JWT-instantie met een opgegeven geheim, uitgever en spelingstijd. Het zorgt ervoor dat het geheim minimaal 32 bytes lang is en geeft een foutmelding als aan deze voorwaarde niet wordt voldaan.
strlen($this->secret) >= 32 || error('JWT secret must be at least 32 bytes', 500)
method

%JWT -> sign (array $claims, int $ttl = 3600):string

line 12
Genereert een JSON Web Token (JWT) door de opgegeven claims te ondertekenen met een gespecificeerde tijdsduur (TTL). De token bevat tijdstempels voor uitgifte (iat) en vervaldatum (exp), en kan optioneel een uitgever (iss) bevatten.
$now = time()
$claims['iat'] = $now
$claims['exp'] = $now + $ttl
if ($this->issuer !== void) $claims['iss'] = $this->issuer
$body = $this->encode(['alg' => 'HS256', 'typ' => 'JWT']).dot.$this->encode($claims)
return $body.dot.$this->sig($body)
method

%JWT -> verify (string $token):array

line 21
Verifieert een JSON Web Token (JWT) door de structuur, handtekening en claims zoals vervaldatum en uitgever te controleren.
$token = preg_replace('/^Bearer\s+/i', void, trim($token))
$parts = explode(dot, $token)
count($parts) === 3 || error('JWT malformed', 401)
[$h, $p, $s] = $parts
$header = (array)json_decode((string)$this->decode($h), true)
($header['alg'] ?? void) === 'HS256' || error('JWT algorithm not allowed', 401)
hash_equals($this->sig($h.dot.$p), $s) || error('JWT signature invalid', 401)
$claims = (array)json_decode((string)$this->decode($p), true)
$now = time()
isset($claims['nbf']) && $now + $this->leeway < $claims['nbf'] && error('JWT not yet valid', 401)
isset($claims['exp']) && $now - $this->leeway >= $claims['exp'] && error('JWT expired', 401)
$this->issuer === void || ($claims['iss'] ?? void) === $this->issuer || error('JWT issuer mismatch', 401)
return $claims
method

%JWT -> sig (string $body):string

line 37
Genereert een handtekening voor de gegeven body met HMAC met SHA-256 en een geheime sleutel.
$this->encode(hash_hmac('sha256', $body, $this->secret, true))
method

%JWT -> encode ($data):string

line 38
Codeert de gegeven gegevens in een JSON Web Token (JWT) formaat met behulp van base64-codering.
rtrim(strtr(base64_encode(is_string($data) ? $data : (string)json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)), '+/', '-_'), eq)
method

%JWT -> decode (string $data):string

line 39
Decodeert een JSON Web Token (JWT) van een base64-gecodeerde string, waarbij URL-veilige tekens worden vervangen door standaard base64-tekens.
(string)base64_decode(strtr($data, '-_', '+/'))
object

%OAuth2

/phlo/resources/security/OAuth2.phlo
version 1.0
creator q-ai.nl
summary Stateless OAuth2 client: build the authorize URL and exchange/refresh tokens. Token storage and config are the caller's responsibility. The protocol primitive under TokenStore and OAuthConnector.
package security
frontend false
backend true
requires HTTP
tags oauth oauth2 token authorization refresh authentication
static

OAuth2 :: authorizeUrl ($endpoint, array $params)

line 10
Genereert de autorisatie-URL voor de OAuth2-stroom door queryparameters aan de eindpunt toe te voegen.
$endpoint.(str_contains($endpoint, '?') ? '&' : '?').http_build_query($params)
static

OAuth2 :: token ($tokenUrl, $clientId, $clientSecret, $grantType, array $extra = [])

line 12
Haal een OAuth2-token op door een verzoek te sturen naar de opgegeven token-URL met de benodigde inloggegevens en parameters, waarbij fouten worden afgehandeld en de reactie als een associatieve array wordt geretourneerd.
$body = ['grant_type' => $grantType, 'client_id' => (string)$clientId, 'client_secret' => (string)$clientSecret]
foreach ($extra AS $key => $value){
	if ($value !== null && $value !== void) $body[$key] = $value
}
try {
	$res = HTTP($tokenUrl, ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'], POST: http_build_query($body))
}
catch (\Throwable $e){
	return ['error' => $e->getMessage()]
}
return json_decode((string)$res, true) ?: ['error' => 'Invalid token response']
static

OAuth2 :: exchangeCode ($tokenUrl, $clientId, $clientSecret, $code, $redirectUri = null, array $extra = [])

line 26
Ruilt een autorisatiecode in voor een toegangstoken met behulp van het OAuth2-protocol.
static::token($tokenUrl, $clientId, $clientSecret, 'authorization_code', ['code' => $code, 'redirect_uri' => $redirectUri] + $extra)
static

OAuth2 :: refresh ($tokenUrl, $clientId, $clientSecret, $refreshToken, array $extra = [])

line 28
Deze methode ververst een OAuth2-toegangstoken met behulp van een opgegeven verversingstoken en aanvullende parameters.
static::token($tokenUrl, $clientId, $clientSecret, 'refresh_token', ['refresh_token' => $refreshToken] + $extra)
object

%rate

/phlo/resources/security/rate.phlo
version 1.0
creator q-ai.nl
summary Rate-limit (fixed window) on the rate_limit table. Schema: resources/security/rate.sql
package security
frontend false
backend true
requires @MySQL
tags rate limit throttle abuse
static

rate :: check ($key, $limit, $windowSeconds, $storage = 'db')

line 10
Controleert de snelheidlimiet voor een gegeven sleutel door de telling in een database te verhogen en ervoor te zorgen dat deze niet boven een opgegeven limiet binnen een gedefinieerd tijdsvenster uitkomt.
if ($storage === 'apcu') return static::checkApcu($key, $limit, $windowSeconds)
$now = time()
// One atomic upsert: increment within the window or reset to a new one, and carry the
// resulting per-request count out via LAST_INSERT_ID so the check needs no separate
// read-then-write (which let parallel requests both pass the limit).
%MySQL->query('INSERT INTO rate_limit (rkey, count, window_start) VALUES (?, LAST_INSERT_ID(1), ?) ON DUPLICATE KEY UPDATE count = LAST_INSERT_ID(IF(? - window_start < ?, count + 1, 1)), window_start = IF(? - window_start < ?, window_start, ?)', $key, $now, $now, $windowSeconds, $now, $windowSeconds, $now)
return (int)%MySQL->query('SELECT LAST_INSERT_ID()')->fetchColumn() <= $limit
static

rate :: checkApcu ($key, $limit, $windowSeconds)

line 20
Controleert of de snelheidslimiet voor een specifieke sleutel is overschreden met behulp van APCu-caching.
$window = (int)floor(time() / $windowSeconds) * $windowSeconds
$apcuKey = 'phlo.rate.'.$key.':'.$window
apcu_add($apcuKey, 0, $windowSeconds)
return apcu_inc($apcuKey, 1) <= $limit
static

rate :: status ($key, $limit, $windowSeconds)

line 27
Haal de huidige status van de rate limit op, inclusief het aantal gebruikte verzoeken, de limiet en de tijd tot de limiet opnieuw instelt.
$now = time()
$row = %MySQL->query('SELECT count, window_start FROM rate_limit WHERE rkey=?', $key)->fetchObject('obj') ?: null
if (!$row || ($now - (int)$row->window_start) >= $windowSeconds) return obj(used: 0, limit: $limit, resetIn: 0)
return obj(used: (int)$row->count, limit: $limit, resetIn: max(0, ((int)$row->window_start + $windowSeconds) - $now))
static

rate :: reset ($key)

line 34
Deze methode reset de rate limit door de bijbehorende invoer uit de rate_limit-tabel in de MySQL-database te verwijderen.
%MySQL->query('DELETE FROM rate_limit WHERE rkey=?', $key)
static

rate :: purge ($olderThanSeconds = 604800)

line 35
Verwijdert vermeldingen uit de rate_limit-tabel waar window_start ouder is dan de opgegeven tijd in seconden.
%MySQL->query('DELETE FROM rate_limit WHERE window_start < ?', time() - $olderThanSeconds)
object

%security

/phlo/resources/security/security.phlo
version 1.0
creator q-ai.nl
summary Generic security resource
package security
frontend true
backend true
requires @session token
tags security csp nonce headers
prop

%security -> whitelist

line 10
Definieert een whitelist voor beveiligingsdoeleinden, waardoor alleen gespecificeerde vermeldingen zijn toegestaan.
[]
method

%security -> setNonce

line 12
Stelt een nonce-waarde in voor de applicatie met behulp van een gegenereerde token van de opgegeven lengte.
%app->nonce = token(8)
method

%security -> frameProtect ($mode = 'DENY')

line 14
Stelt de X-Frame-Options-header in om te bepalen of de pagina in een frame kan worden weergegeven, met de standaardmodus 'DENY'.
%res->header('X-Frame-Options', $mode)
method

%security -> frameWhitelist

line 15
Genereert een string van goedgekeurde frame-oorsprongen voor beveiligingsdoeleinden, waardoor alleen opgegeven domeinen de inhoud in een iframe kunnen insluiten.
$this->whitelist ? ' '.implode(space, array_map(fn($d) => "https://*.$d", (array)$this->whitelist)) : void
method

%security -> strict

line 17
Stelt strikte beveiligingsheaders in voor HTTP-responses, waaronder Cache-Control en Content-Security-Policy, om de beveiliging tegen verschillende aanvallen te verbeteren.
$this->base
if (%req->async) return
%res->header('Cache-Control', 'no-store')
$nonce = $this->setNonce
%res->header('Content-Security-Policy', "default-src 'self'; script-src 'nonce-$nonce'; worker-src 'self'; style-src 'self' 'nonce-$nonce'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; object-src 'none'; frame-src 'self'$this->frameWhitelist; frame-ancestors 'none'; base-uri 'self'")
method

%security -> basic

line 26
Stelt basisbeveiligingsheaders in voor de respons, inclusief een Content-Security-Policy die het laden van bronnen beperkt tot dezelfde oorsprong en toegestane bronnen voor scripts, stijlen, afbeeldingen, lettertypen en verbindingen specificeert.
$this->base
if (%req->async) return
%res->header('Content-Security-Policy', "default-src 'self'; script-src 'self'".(debug ? " 'unsafe-inline'" : void)."; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; object-src 'none'; frame-src 'self'$this->frameWhitelist; frame-ancestors 'none'; base-uri 'self'")
method

%security -> marketing

line 31
Stelt de headers voor het Content Security Policy in voor de respons, waarbij toegestane bronnen voor verschillende contenttypes worden gedefinieerd op basis van de async-status van het verzoek en de debugmodus.
$this->base
if (%req->async) return
%res->header('Content-Security-Policy', "default-src 'self'; script-src 'self'".(debug ? " 'unsafe-inline'" : void)."; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; form-action 'self'; object-src 'none'; frame-src 'self'$this->frameWhitelist; frame-ancestors 'none'; base-uri 'self'")
method

%security -> api

line 36
Stelt beveiligingsgerelateerde HTTP-headers in voor de API-respons, waardoor de bescherming tegen verschillende webkwetsbaarheden wordt verbeterd.
%res->api = true
%res->header('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'")
%res->header('X-Content-Type-Options', 'nosniff')
%res->header('Referrer-Policy', 'no-referrer')
method

%security -> base

line 43
Stelt verschillende beveiligingsgerelateerde HTTP-headers in voor de respons, waaronder Referrer-Policy, X-Content-Type-Options, Cross-Origin-beleid en X-Frame-Options, om de beveiliging tegen cross-origin aanvallen te verbeteren.
%res->header('Referrer-Policy', 'strict-origin-when-cross-origin')
%res->header('X-Content-Type-Options', 'nosniff')
%req->async || %res->header('Cross-Origin-Opener-Policy', 'same-origin')
%req->async || %res->header('Cross-Origin-Resource-Policy', 'same-origin')
%req->async || %res->header('Access-Control-Allow-Origin', %req->base)
%req->async || %res->header('X-Frame-Options', 'DENY')
object

%social

/phlo/resources/security/social.phlo
version 1.0
creator q-ai.nl
summary Reusable social login (OIDC) on top of OAuth2: build the authorize URL and turn a callback code into a verified profile. Google, Microsoft and Apple. No user, session or route handling - that is the caller's responsibility.
package security
frontend false
backend true
requires OAuth2 creds payload HTTP php-ext:openssl
tags oauth oidc social login google microsoft apple authentication
advice The id_token is verified against the provider's JWKS (RS256 only, key looked up by kid) before any claim is read, on top of issuer, audience (must equal client_id), expiry and, when supplied, nonce. `verified` reports what the provider actually proved: Microsoft omits email_verified, so it counts only when the optional xms_edov claim states the tenant owns the address. Treat an unverified email as a claim, never as an identity: match users on provider + sub. Apple's client_secret is an ES256 JWT signed with the .p8 key (team_id/key_id/client_id + key from creds).
static

social :: providers

line 11
arr(
	google: arr(
		label: 'Google',
		authorize: 'https://accounts.google.com/o/oauth2/v2/auth',
		token: 'https://oauth2.googleapis.com/token',
		jwks: 'https://www.googleapis.com/oauth2/v3/certs',
		scope: 'openid email profile',
		issuer: 'https://accounts.google.com',
	),
	microsoft: arr(
		label: 'Microsoft',
		authorize: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
		token: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
		jwks: 'https://login.microsoftonline.com/common/discovery/v2.0/keys',
		scope: 'openid email profile',
		issuer: 'https://login.microsoftonline.com/',
	),
	apple: arr(
		label: 'Apple',
		authorize: 'https://appleid.apple.com/auth/authorize',
		token: 'https://appleid.apple.com/auth/token',
		jwks: 'https://appleid.apple.com/auth/keys',
		scope: 'name email',
		issuer: 'https://appleid.apple.com',
	),
)
static

social :: config ($provider)

line 38
$p = static::providers()[$provider] ?? null
if (!$p) return null
$section = %creds->{$provider} ?? null
$creds = is_object($section) ? (array)$section->toArray : []
return $p + arr(
	client_id: (string)($creds['client_id'] ?? void),
	client_secret: (string)($creds['client_secret'] ?? void),
	redirect_uri: (string)($creds['redirect_uri'] ?? ('https://'.%req->host.'/auth/'.$provider.'/callback')),
)
static

social :: configured ($provider)

line 50
Controleert of de sociale configuratie voor een gegeven provider is ingesteld en of de client-ID niet leeg is.
$c = static::config($provider)
return $c && $c['client_id'] !== void
static

social :: authUrl ($provider, $state, $nonce = void)

line 55
$c = static::config($provider)
$params = arr(
	client_id: $c['client_id'],
	redirect_uri: $c['redirect_uri'],
	response_type: 'code',
	scope: $c['scope'],
	state: $state,
)
if ($nonce !== void) $params['nonce'] = $nonce
if ($provider === 'apple') $params['response_mode'] = 'form_post'
return OAuth2::authorizeUrl($c['authorize'], $params)
static

social :: profile ($provider, $code, $nonce = void)

line 69
$cfg = static::config($provider)
if (!$cfg || $cfg['client_id'] === void) return null
$secret = $provider === 'apple' ? static::appleSecret() : $cfg['client_secret']
$token = OAuth2::exchangeCode($cfg['token'], $cfg['client_id'], $secret, $code, $cfg['redirect_uri'])
$jwt = (string)($token['id_token'] ?? void)
if (!static::verifySignature($provider, $jwt)) return null
$claims = static::decodeIdToken($jwt)
if (!$claims || !static::verifyClaims($provider, $cfg, $claims, $nonce)) return null
$profile = static::normalize($provider, $claims)
if ($provider === 'apple' && !$profile['name']){
	$u = json_decode((string)(%payload->user ?? void), true)
	if (is_array($u) && !empty($u['name'])) $profile['name'] = trim(((string)($u['name']['firstName'] ?? void)).space.((string)($u['name']['lastName'] ?? void)))
}
return $profile
static

social :: decodeIdToken ($jwt)

line 86
$parts = explode(dot, (string)$jwt)
if (count($parts) < 2) return null
$claims = json_decode(static::b64urlDecode($parts[1]), true)
return is_array($claims) ? $claims : null
static

social :: algs

line 93
arr(RS256: OPENSSL_ALGO_SHA256)
static

social :: verifySignature ($provider, $jwt)

line 95
$parts = explode(dot, (string)$jwt)
if (count($parts) !== 3) return false
$header = json_decode(static::b64urlDecode($parts[0]), true)
if (!is_array($header)) return false
$alg = static::algs()[(string)($header['alg'] ?? void)] ?? null
$kid = (string)($header['kid'] ?? void)
if (!$alg || !$kid) return false
$pem = static::key($provider, $kid)
if (!$pem) return false
return openssl_verify($parts[0].dot.$parts[1], static::b64urlDecode($parts[2]), $pem, $alg) === 1
static

social :: jwks ($provider)

line 108
$cached = %req->socialJwks ?? []
if (isset($cached[$provider])) return $cached[$provider]
$url = (string)(static::providers()[$provider]['jwks'] ?? void)
$keys = []
if ($url){
	$data = json_decode((string)HTTP($url, ['Accept: application/json']), true)
	if (is_array($data) && is_array($data['keys'] ?? null)) $keys = $data['keys']
}
$cached[$provider] = $keys
%req->socialJwks = $cached
return $keys
static

social :: key ($provider, $kid)

line 122
foreach (static::jwks($provider) AS $jwk){
	if ((string)($jwk['kid'] ?? void) === $kid) return static::jwkToPem((array)$jwk)
}
return void
static

social :: jwkToPem (array $jwk)

line 129
if ((string)($jwk['kty'] ?? void) !== 'RSA') return void
$n = static::b64urlDecode((string)($jwk['n'] ?? void))
$e = static::b64urlDecode((string)($jwk['e'] ?? void))
if (!$n || !$e) return void
$key = static::der(0x30, static::derInt($n).static::derInt($e))
$bits = static::der(0x03, "\x00".$key)
$algo = static::der(0x30, "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00")
$spki = static::der(0x30, $algo.$bits)
return '-----BEGIN PUBLIC KEY-----'.lf.chunk_split(base64_encode($spki), 64, lf).'-----END PUBLIC KEY-----'.lf
static

social :: der ($tag, $content)

line 141
$len = strlen($content)
if ($len < 0x80) return chr($tag).chr($len).$content
$bytes = ltrim(pack('N', $len), "\x00")
return chr($tag).chr(0x80 | strlen($bytes)).$bytes.$content
static

social :: derInt ($bytes)

line 148
$bytes = ltrim((string)$bytes, "\x00")
if ($bytes === void) $bytes = "\x00"
if (ord($bytes[0]) & 0x80) $bytes = "\x00".$bytes
return static::der(0x02, $bytes)
static

social :: b64urlDecode ($data)

line 155
$data = strtr((string)$data, '-_', '+/')
$pad = strlen($data) % 4
if ($pad) $data .= str_repeat(eq, 4 - $pad)
return (string)base64_decode($data)
static

social :: verifyClaims ($provider, array $cfg, array $claims, $nonce = void)

line 162
if ((int)($claims['exp'] ?? 0) <= time()) return false
$aud = $claims['aud'] ?? void
if (!in_array((string)($cfg['client_id'] ?? void), array_map('strval', is_array($aud) ? $aud : [$aud]), true)) return false
if ($nonce !== void && !hash_equals((string)$nonce, (string)($claims['nonce'] ?? void))) return false
return static::verifyIssuer($provider, $claims)
static

social :: verifyIssuer ($provider, array $claims)

line 170
$iss = (string)($claims['iss'] ?? void)
if ($provider === 'microsoft'){
	$tid = (string)($claims['tid'] ?? void)
	return $tid !== void && $iss === 'https://login.microsoftonline.com/'.$tid.'/v2.0'
}
$expected = (string)(static::providers()[$provider]['issuer'] ?? void)
if ($expected === void) return false
return $iss === $expected || ($provider === 'google' && $iss === 'accounts.google.com')
static

social :: normalize ($provider, array $claims)

line 181
$ev = $claims['email_verified'] ?? null
$verified = $ev === true || $ev === 'true'
if ($provider === 'microsoft'){
	$edov = $claims['xms_edov'] ?? null
	$verified = $edov === true || $edov === 'true' || $edov === 1 || $edov === '1'
}
$name = (string)($claims['name'] ?? void)
if (!$name) $name = trim(((string)($claims['given_name'] ?? void)).space.((string)($claims['family_name'] ?? void)))
return arr(
	provider: $provider,
	uid: (string)($claims['sub'] ?? void),
	email: strtolower(trim((string)($claims['email'] ?? void))),
	verified: $verified,
	name: trim($name),
)
static

social :: b64url ($data)

line 199
Codeert de gegeven data in een base64 URL-veilige indeling door '+' te vervangen door '-', '/' door '_' en eventuele trailing gelijktekens te verwijderen.
rtrim(strtr(base64_encode((string)$data), '+/', '-_'), eq)
static

social :: appleSecret

line 201
$section = %creds->apple ?? null
$creds = is_object($section) ? (array)$section->toArray : []
$teamId = (string)($creds['team_id'] ?? void)
$keyId = (string)($creds['key_id'] ?? void)
$clientId = (string)($creds['client_id'] ?? void)
$keyData = (string)($creds['private_key'] ?? void)
if (!$keyData && !empty($creds['key_file']) && is_file((string)$creds['key_file'])) $keyData = (string)file_get_contents((string)$creds['key_file'])
if (!$teamId || !$keyId || !$clientId || !$keyData) return void
$key = openssl_pkey_get_private($keyData)
if (!$key) return void
$now = time()
$input = static::b64url(json_encode(arr(alg: 'ES256', kid: $keyId))).dot.static::b64url(json_encode(arr(iss: $teamId, iat: $now, exp: $now + 15552000, aud: 'https://appleid.apple.com', sub: $clientId)))
$der = void
if (!openssl_sign($input, $der, $key, OPENSSL_ALGO_SHA256)) return void
return $input.dot.static::b64url(static::derToJose($der))
static

social :: derToJose ($der)

line 219
Extraheert de 'r' en 's' waarden uit een DER-gecodeerde handtekening en vult deze aan tot 32 bytes.
$rlen = ord($der[3])
$r = substr((string)$der, 4, $rlen)
$slen = ord($der[4 + $rlen + 1])
$s = substr((string)$der, 4 + $rlen + 2, $slen)
return static::pad32($r).static::pad32($s)
static

social :: pad32 ($x)

line 227
Deze methode voegt null-bytes aan de linkerkant van een string toe om ervoor te zorgen dat deze 32 tekens lang is, waarbij eventuele leidende null-bytes voor het opvullen worden verwijderd.
$x = ltrim((string)$x, "\x00")
return str_pad($x, 32, "\x00", STR_PAD_LEFT)

Functions

function

decrypt($encrypted, $key):string|false

/phlo/resources/security/encryption.phlo line 11
Decrypt de gegeven base64-gecodeerde versleutelde string met de opgegeven sleutel en retourneer de oorspronkelijke gegevens of false als de decryptie mislukt.
($d = base64_decode($encrypted, true)) !== false && strlen($d) >= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ? sodium_crypto_secretbox_open(substr($d, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), substr($d, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), hash('sha256', $key, true)) : false
function

encrypt($data, $key):string

/phlo/resources/security/encryption.phlo line 9
Versleutelt de gegeven data met een geheime sleutel en retourneert het base64-gecodeerde resultaat.
base64_encode(($nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES)).sodium_crypto_secretbox($data, $nonce, hash('sha256', $key, true)))
function

token(int $length = 8, ?string $input = null)

/phlo/resources/security/token.phlo line 9
Genereert een willekeurig token van de opgegeven lengte met een gedefinieerd alfabet, optioneel gezaaid met een invoerstring voor extra willekeurigheid.
	$length || error('Token must have a minimum length above 0', 500)
	$alphabet = 'abcdefghijklmnopqrstuvwxyz'
	$alphabetLength = strlen($alphabet)
	$limit = intdiv(256, $alphabetLength) * $alphabetLength
	$token = void
	$buffer = void
	$state = is_null($input) ? null : hash('sha256', (string)$input, true)
	while (strlen($token) < $length){
		if ($buffer === void){
			if (is_null($state)) $buffer = random_bytes(32)
			else {
				$state = hash('sha256', $state, true)
				$buffer = $state
			}
		}
		$byte = ord($buffer[0])
		$buffer = substr($buffer, 1)
		if ($byte >= $limit) continue
		$token .= $alphabet[$byte % $alphabetLength]
	}
	return $token

We gebruiken essentiële cookies om deze site te laten werken. Met uw toestemming gebruiken we ook analytics om de site te verbeteren.