Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b67bf574d | ||
|
|
fc80ee1659 | ||
|
|
1d62f0affd | ||
|
|
ddc08270a7 | ||
|
|
32208c7701 | ||
|
|
62544d86b4 | ||
|
|
35d58ff07b | ||
|
|
464524da27 | ||
|
|
8bb3c1ce12 | ||
|
|
bbcada1317 | ||
|
|
878e2b98e6 | ||
|
|
6cc2ddabd0 | ||
|
|
fb60a1eccb | ||
|
|
ebea82f63d | ||
|
|
3d3493e546 | ||
|
|
822b9b93c5 | ||
|
|
ba8868cc12 | ||
|
|
78606bf751 | ||
|
|
d97638201e | ||
|
|
df7ca1329e | ||
|
|
8b7f11fc6a | ||
|
|
cdef7da4b9 | ||
|
|
f51997016d | ||
|
|
49265eee0f | ||
|
|
4d612b57f4 | ||
|
|
f9323110ce | ||
|
|
12cefe68e8 | ||
|
|
cb0bd43d04 | ||
|
|
78a18ccfab | ||
|
|
7bb919a0a0 | ||
|
|
60f6989595 | ||
|
|
066a0b8fb7 | ||
|
|
d4a6418e5d | ||
|
|
7d0319332f | ||
|
|
44aa9e6f80 | ||
|
|
d8a28dea12 | ||
|
|
f529714f45 |
@@ -40,7 +40,7 @@ In the consuming application's `composer.json`:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"dixon/blackwall-auth-sdk": "*"
|
"andydixon/blackwall-auth-sdk": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -88,6 +88,7 @@ exit;
|
|||||||
```
|
```
|
||||||
|
|
||||||
For callback handling, see `docs/INTEGRATION_GUIDE.md`.
|
For callback handling, see `docs/INTEGRATION_GUIDE.md`.
|
||||||
|
For provider-side threat/invariant assumptions, see `docs/SECURITY_MODEL.md`.
|
||||||
|
|
||||||
Minimal callback example:
|
Minimal callback example:
|
||||||
|
|
||||||
@@ -128,7 +129,27 @@ This wrapper now delegates to `BlackWall\Auth\AuthClient`.
|
|||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
- Always validate OAuth `state` in callback handlers.
|
- Always validate OAuth `state` in callback handlers.
|
||||||
|
- Handle provider callback errors (`error`, `error_description`) explicitly; treat `access_denied` as an expected user/authorisation outcome rather than a transport failure.
|
||||||
|
- Treat `invalid_scope` as a client-configuration mismatch: requested scopes must be a subset of scopes allowed on the provider client.
|
||||||
|
- Expect `access_denied` even after the consent page is displayed if provider-side project membership or tenant scope changes before consent submission.
|
||||||
|
- Expect provider token introspection to return `active=false` when user/project/client state is no longer active.
|
||||||
|
- Expect revoked JWT access tokens to be rejected by provider `userinfo` even before token expiry.
|
||||||
|
- Register only HTTPS redirect URIs in provider client settings (HTTP should be used only for localhost loopback during development).
|
||||||
|
- Use OAuth/portal authentication endpoints for end users; provider admin login endpoints enforce separate admin scope checks.
|
||||||
|
- For provider admin enrolment URL export operations, use `POST` with CSRF protection; do not automate them via unauthenticated `GET` links.
|
||||||
|
- Handle provider CSV exports with formula-injection safety in mind if opening them in spreadsheet applications.
|
||||||
|
- Treat generated enrolment URLs as secrets and keep them out of URL query strings and request logs.
|
||||||
|
- For reverse-proxy deployments, harden trusted proxy/IP forwarding configuration to prevent spoofed client IP headers.
|
||||||
|
- Prefer narrowly scoped trusted-proxy entries (explicit IPs/CIDR ranges) instead of broad network trust.
|
||||||
|
- Keep client-side admin mutation retries idempotent; approval workflows can be lock-serialised and should not be assumed to execute twice.
|
||||||
|
- Do not submit parallel approve/reject/cancel decisions for the same approval request ID.
|
||||||
|
- Apply bounded CSV import sizes and row counts for admin bulk-user operations.
|
||||||
|
- Never place client secrets, refresh tokens, or access tokens in URL query strings.
|
||||||
- For OIDC providers, include a per-request `nonce` in authorisation requests.
|
- For OIDC providers, include a per-request `nonce` in authorisation requests.
|
||||||
|
- Ensure requested `scope` values in SDK config/examples (for example `offline_access`) are enabled on the provider-side client before rollout.
|
||||||
|
- Expect refresh-token exchange to fail (`invalid_grant`) if provider-side client scopes were tightened and the refresh token now carries disallowed scopes.
|
||||||
|
- For direct Cryptbin API usage, send `key_b64url` on unwrap calls; provider rejects key-mismatch unwrap attempts with `403 Forbidden`.
|
||||||
|
- For newly created Cryptbin items, continue using the creating WebAuthn credential for unwrap/update/delete flows.
|
||||||
- Always use HTTPS in production.
|
- Always use HTTPS in production.
|
||||||
- Store refresh tokens securely.
|
- Store refresh tokens securely.
|
||||||
- Keep access tokens out of logs and browser-visible output.
|
- Keep access tokens out of logs and browser-visible output.
|
||||||
|
|||||||
@@ -63,9 +63,13 @@ $client = new AuthClient(Config::fromArray([
|
|||||||
]));
|
]));
|
||||||
|
|
||||||
if (isset($_GET['error'])) {
|
if (isset($_GET['error'])) {
|
||||||
|
$error = (string) $_GET['error'];
|
||||||
$description = isset($_GET['error_description']) ? (string) $_GET['error_description'] : 'Unknown provider error';
|
$description = isset($_GET['error_description']) ? (string) $_GET['error_description'] : 'Unknown provider error';
|
||||||
http_response_code(400);
|
// access_denied is a normal authorisation outcome (for example, membership
|
||||||
exit('Authorization failed: ' . $description);
|
// revoked, privilege changed, or user denied consent between request and submit).
|
||||||
|
// invalid_scope usually means the app requested scopes not enabled on the provider client.
|
||||||
|
http_response_code(in_array($error, ['access_denied', 'invalid_scope'], true) ? 403 : 400);
|
||||||
|
exit('Authorisation failed: ' . $description);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $client->handleCallback($_GET);
|
$result = $client->handleCallback($_GET);
|
||||||
@@ -131,13 +135,45 @@ try {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`UserInfoException` with `invalid_token` can occur when provider-side subject scope is no longer active (for example user/project disabled) between token issuance and userinfo retrieval.
|
||||||
|
`TokenExchangeException` with `invalid_grant` can also occur when provider-side client/user/project state is inactive at exchange time.
|
||||||
|
Token introspection can return `active=false` when the token subject (user/project) or client has since been disabled.
|
||||||
|
Provider JWT access tokens are now revocation-aware at `userinfo`; revoked JWTs can fail before natural expiry.
|
||||||
|
Refresh-token exchange can return `invalid_grant` when provider client scope policy has been tightened and stored refresh scopes are no longer allowed.
|
||||||
|
Provider login/authorisation steps can also fail with `access_denied` when account status is disabled before completion.
|
||||||
|
WebAuthn-based login verification can return credential-style failures for disabled accounts even when authenticator assertions are otherwise valid.
|
||||||
|
Existing authenticated sessions may also be invalidated when account status changes to disabled.
|
||||||
|
OAuth authorisation consent submission can return `access_denied` if the account becomes inactive between session establishment and consent completion.
|
||||||
|
OAuth authorisation can also return `invalid_scope` if requested scopes are not explicitly allowed on the provider client configuration.
|
||||||
|
Provider user/project assignment views may exclude disabled projects even if historical assignments exist.
|
||||||
|
|
||||||
## Operational guidance
|
## Operational guidance
|
||||||
|
|
||||||
- Keep provider URLs in environment variables, not hard-coded.
|
- Keep provider URLs in environment variables, not hard-coded.
|
||||||
- Avoid printing tokens in production pages.
|
- Avoid printing tokens in production pages.
|
||||||
|
- Expect `429 Too Many Requests` from provider control endpoints under abuse protection; implement backoff/retry instead of tight loops.
|
||||||
|
- Apply the same backoff strategy for provider `userinfo` calls that return `429 Too Many Requests`.
|
||||||
|
- For token exchange and refresh, apply bounded retry/backoff on transient provider failures instead of immediate repeated retries.
|
||||||
|
- Use exactly one client authentication method per token/control request (do not send both HTTP Basic and `client_secret` form fields together).
|
||||||
|
- If you call provider WebAuthn login challenge/verify endpoints directly, use `POST` only.
|
||||||
|
- Treat admin enrolment URL export as a state-changing operation: call it with `POST` and include a valid CSRF token.
|
||||||
|
- Treat provider CSV exports as untrusted input in spreadsheet tools; prefer importing into systems that neutralise formula cells.
|
||||||
|
- Treat enrolment URLs as sensitive bearer material: do not place them in redirect query parameters, logs, or analytics tags; keep them in one-time server-side flash/session state.
|
||||||
|
- If your deployment runs behind reverse proxies, ensure trusted-proxy configuration is strict so spoofed forwarding headers cannot bypass IP-based abuse controls.
|
||||||
|
- Trusted proxy allowlists may be expressed as explicit IPs or CIDR ranges; avoid broad ranges and keep proxy chains minimal.
|
||||||
|
- For approval-driven admin mutations, assume provider-side execution is serialised per approval item; client retries should still be idempotent and bounded.
|
||||||
|
- This serialisation applies across approval actions (approve/reject/cancel), so clients should avoid parallel decision submissions for the same request ID.
|
||||||
|
- For provider CSV-based user import operations, enforce strict file-size and row-count limits in automation tooling and surface clear operator errors on limit breaches.
|
||||||
|
- Configure OAuth client redirect URIs as HTTPS in production; reserve HTTP for localhost loopback testing only.
|
||||||
|
- Assume provider rate limits are enforced per source identity (for example IP) and shared across related auth endpoints.
|
||||||
|
- Treat report/export-style provider endpoints as high-cost and expect stricter rate limits.
|
||||||
- Rotate client secrets for confidential clients.
|
- Rotate client secrets for confidential clients.
|
||||||
|
- Do not transport secrets or tokens in URL query parameters; keep them in server-side session or secure storage only.
|
||||||
- Add a unique `nonce` to each OIDC authorisation request.
|
- Add a unique `nonce` to each OIDC authorisation request.
|
||||||
|
- Keep `state`, `nonce`, and PKCE values within standard URL-safe formats/lengths; malformed values can be rejected by provider validation.
|
||||||
- Validate `state` on every callback request.
|
- Validate `state` on every callback request.
|
||||||
- Use secure session cookies (`Secure`, `HttpOnly`, `SameSite=Lax` or stricter).
|
- Use secure session cookies (`Secure`, `HttpOnly`, `SameSite=Lax` or stricter).
|
||||||
- HTTPS URLs are enforced by default in `Config::fromArray()`.
|
- HTTPS URLs are enforced by default in `Config::fromArray()`.
|
||||||
- For localhost-only HTTP testing, explicitly set `allowInsecureHttp => true`.
|
- For localhost-only HTTP testing, explicitly set `allowInsecureHttp => true`.
|
||||||
|
- If you call Cryptbin unwrap APIs directly, include the original share key as `key_b64url`; unwrap now returns `403 Forbidden` when key proof does not match metadata verifier.
|
||||||
|
- New Cryptbin items may also bind unwrap/update/delete operations to the creating WebAuthn credential; use the same authenticator for follow-up operations.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ $user = $client->getUserInfo($tokens->accessToken);
|
|||||||
## Key differences
|
## Key differences
|
||||||
|
|
||||||
- Method spelling uses British English in the new API: `buildAuthorisationUrl()`.
|
- Method spelling uses British English in the new API: `buildAuthorisationUrl()`.
|
||||||
|
- The legacy method name `getAuthorizationUrl()` is unchanged for backwards compatibility.
|
||||||
- Token responses are strongly typed via `TokenSet`.
|
- Token responses are strongly typed via `TokenSet`.
|
||||||
- Errors are represented by specific exception classes.
|
- Errors are represented by specific exception classes.
|
||||||
- HTTP transport is swappable via `HttpClientInterface`.
|
- HTTP transport is swappable via `HttpClientInterface`.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# BlackWall Provider Security Model (SDK View)
|
||||||
|
|
||||||
|
Last reviewed: 2026-02-26
|
||||||
|
|
||||||
|
This document summarises provider-side security guarantees that SDK integrators should design around.
|
||||||
|
|
||||||
|
## Threat Assumptions
|
||||||
|
|
||||||
|
- Provider is internet-exposed.
|
||||||
|
- Attackers can register and attempt tenant-boundary abuse.
|
||||||
|
- OAuth/WebAuthn endpoints are abuse targets and may rate limit.
|
||||||
|
|
||||||
|
## Invariants Integrators Should Assume
|
||||||
|
|
||||||
|
1. Tenant/project/user/client scope is enforced server-side and can change between request steps.
|
||||||
|
2. OAuth authorize can fail with `invalid_scope` if requested scopes are not currently allowed for the client.
|
||||||
|
3. Token/refresh exchange can fail with `invalid_grant` when subject/client state or scope policy changes.
|
||||||
|
4. UserInfo can fail for revoked/invalid/inactive subjects even before nominal token expiry.
|
||||||
|
5. Admin and portal workflows are distinct scope domains; do not mix them in client UX flows.
|
||||||
|
|
||||||
|
## Integration Guidance
|
||||||
|
|
||||||
|
- Keep requested scopes minimal and aligned to the provider client configuration.
|
||||||
|
- Treat `access_denied`, `invalid_scope`, and `invalid_grant` as expected security outcomes, not transport errors.
|
||||||
|
- Build retries only for transient failures; do not blindly retry authz/authn denials.
|
||||||
|
- Keep enrolment URLs/tokens and OAuth tokens out of URLs, logs, and analytics tags.
|
||||||
|
- Use strict HTTPS and secure session-cookie handling in the relying party app.
|
||||||
@@ -16,6 +16,7 @@ foreach ($autoloadCandidates as $file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use BlackWallSDK\BlackWallAuth;
|
use BlackWallSDK\BlackWallAuth;
|
||||||
|
use BlackWall\Auth\Exception\UserInfoException;
|
||||||
|
|
||||||
$auth = new BlackWallAuth([
|
$auth = new BlackWallAuth([
|
||||||
'clientId' => '94d228c4-d4c6-4479-9b02-793e6a73e3f2',
|
'clientId' => '94d228c4-d4c6-4479-9b02-793e6a73e3f2',
|
||||||
@@ -25,6 +26,15 @@ $auth = new BlackWallAuth([
|
|||||||
'redirectUri' => 'https://test.dixon.cx/callback.php',
|
'redirectUri' => 'https://test.dixon.cx/callback.php',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (isset($_GET['error'])) {
|
||||||
|
$error = (string) $_GET['error'];
|
||||||
|
$description = isset($_GET['error_description']) ? (string) $_GET['error_description'] : 'Authorisation was denied.';
|
||||||
|
$status = $error === 'access_denied' ? 403 : 400;
|
||||||
|
http_response_code($status);
|
||||||
|
echo 'Authorisation failed: ' . htmlspecialchars($description, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = $auth->handleCallback($_GET);
|
$result = $auth->handleCallback($_GET);
|
||||||
$_SESSION['access_token'] = $result['tokens']['access_token'] ?? null;
|
$_SESSION['access_token'] = $result['tokens']['access_token'] ?? null;
|
||||||
@@ -33,6 +43,9 @@ try {
|
|||||||
|
|
||||||
header('Location: /');
|
header('Location: /');
|
||||||
exit;
|
exit;
|
||||||
|
} catch (UserInfoException $e) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo 'User info access failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo 'Auth failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
echo 'Auth failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
|||||||
Reference in New Issue
Block a user