Adds integration guidance that refresh-token exchange may fail after provider-side client scope policy is narrowed.
180 lines
8.5 KiB
Markdown
180 lines
8.5 KiB
Markdown
# Integration Guide (British English)
|
|
|
|
This guide shows a complete OAuth/OIDC login flow using `BlackWall\Auth\AuthClient`.
|
|
|
|
## 1. Start login
|
|
|
|
```php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
session_start();
|
|
require __DIR__ . '/vendor/autoload.php';
|
|
|
|
use BlackWall\Auth\AuthClient;
|
|
use BlackWall\Auth\Config;
|
|
|
|
$client = new AuthClient(Config::fromArray([
|
|
'clientId' => 'your-client-id',
|
|
'authorizeUrl' => 'https://blackwall.cx/oauth/authorize',
|
|
'tokenUrl' => 'https://blackwall.cx/oauth/token',
|
|
'userInfoUrl' => 'https://blackwall.cx/oauth/userinfo',
|
|
'redirectUri' => 'https://your-app.example/callback.php',
|
|
'scope' => 'openid profile email offline_access',
|
|
]));
|
|
|
|
if (!isset($_SESSION['user'])) {
|
|
$auth = $client->buildAuthorisationUrl([
|
|
'extra' => [
|
|
// Some providers require nonce for OIDC requests.
|
|
'nonce' => rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='),
|
|
],
|
|
]);
|
|
// Prevent stale redirects from being reused from cache.
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
header('Pragma: no-cache');
|
|
header('Location: ' . $auth['url']);
|
|
exit;
|
|
}
|
|
|
|
echo 'Already signed in.';
|
|
```
|
|
|
|
## 2. Handle callback
|
|
|
|
```php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
session_start();
|
|
require __DIR__ . '/vendor/autoload.php';
|
|
|
|
use BlackWall\Auth\AuthClient;
|
|
use BlackWall\Auth\Config;
|
|
|
|
$client = new AuthClient(Config::fromArray([
|
|
'clientId' => 'your-client-id',
|
|
'authorizeUrl' => 'https://blackwall.cx/oauth/authorize',
|
|
'tokenUrl' => 'https://blackwall.cx/oauth/token',
|
|
'userInfoUrl' => 'https://blackwall.cx/oauth/userinfo',
|
|
'redirectUri' => 'https://your-app.example/callback.php',
|
|
]));
|
|
|
|
if (isset($_GET['error'])) {
|
|
$error = (string) $_GET['error'];
|
|
$description = isset($_GET['error_description']) ? (string) $_GET['error_description'] : 'Unknown provider error';
|
|
// access_denied is a normal authorisation outcome (for example, membership
|
|
// 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('Authorization failed: ' . $description);
|
|
}
|
|
|
|
$result = $client->handleCallback($_GET);
|
|
|
|
$_SESSION['user'] = [
|
|
'email' => $result->user->email,
|
|
'privilege_level' => $result->user->privilegeLevel,
|
|
'role' => $result->user->role,
|
|
];
|
|
$_SESSION['access_token'] = $result->tokens->accessToken;
|
|
$_SESSION['refresh_token'] = $result->tokens->refreshToken;
|
|
|
|
header('Location: /');
|
|
exit;
|
|
```
|
|
|
|
## 3. Single callback for multiple app roles
|
|
|
|
Using one OAuth client and one callback URL is recommended.
|
|
|
|
Map users in your application by:
|
|
|
|
1. `email` claim to local account record
|
|
2. `privilege_level` (or `role`) to application role
|
|
|
|
Typical mapping example:
|
|
|
|
- `privilege_level = 1` => admin/superadmin
|
|
- `privilege_level = 2` => user/tutor
|
|
|
|
If `privilege_level` is absent, the SDK also resolves common role strings (`admin`, `superadmin`, `user`, `tutor`).
|
|
|
|
## 4. Refresh token (optional)
|
|
|
|
```php
|
|
<?php
|
|
|
|
$tokens = $client->refreshAccessToken($_SESSION['refresh_token']);
|
|
$_SESSION['access_token'] = $tokens->accessToken;
|
|
if ($tokens->refreshToken !== null) {
|
|
$_SESSION['refresh_token'] = $tokens->refreshToken;
|
|
}
|
|
```
|
|
|
|
## Exceptions
|
|
|
|
Catch specific exceptions for clearer handling:
|
|
|
|
- `BlackWall\Auth\Exception\StateMismatchException`
|
|
- `BlackWall\Auth\Exception\TokenExchangeException`
|
|
- `BlackWall\Auth\Exception\UserInfoException`
|
|
- `BlackWall\Auth\Exception\TransportException`
|
|
|
|
Example:
|
|
|
|
```php
|
|
try {
|
|
$tokens = $client->exchangeCodeForTokens($code);
|
|
} catch (\BlackWall\Auth\Exception\TokenExchangeException $e) {
|
|
error_log('Token exchange failed: ' . $e->getMessage());
|
|
error_log('Auth code: ' . ($e->authCode() ?? 'none'));
|
|
http_response_code(502);
|
|
}
|
|
```
|
|
|
|
`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/authorization 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 authorization consent submission can return `access_denied` if the account becomes inactive between session establishment and consent completion.
|
|
OAuth authorization 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
|
|
|
|
- Keep provider URLs in environment variables, not hard-coded.
|
|
- 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 enrollment 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 enrollment 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 serialized per approval item; client retries should still be idempotent and bounded.
|
|
- This serialization 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.
|
|
- 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.
|
|
- 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.
|
|
- Use secure session cookies (`Secure`, `HttpOnly`, `SameSite=Lax` or stricter).
|
|
- HTTPS URLs are enforced by default in `Config::fromArray()`.
|
|
- 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.
|