Skip to content
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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ mainick_keycloak_client:
encryption_key: '%env(IAM_ENCRYPTION_KEY)%'
encryption_key_path: '%env(IAM_ENCRYPTION_KEY_PATH)%'
version: '%env(IAM_VERSION)%'
# Optional: Whitelist of allowed domains for JWKS endpoint (security feature)
# If not specified, only the domain from base_url is allowed
allowed_jwks_domains:
- 'keycloak.example.com'
- '*.auth.example.com' # Supports wildcard subdomains
```

Additionally, it's recommended to add the following environment variables to your project's environment file
Expand All @@ -44,7 +49,7 @@ IAM_REALM='<your-realm>' # Keycloak realm name
IAM_CLIENT_ID='<your-client-id>' # Keycloak client id
IAM_CLIENT_SECRET='<your-client-secret>' # Keycloak client secret
IAM_REDIRECT_URI='<your-redirect-uri>' # Keycloak redirect uri
IAM_ENCRYPTION_ALGORITHM='<your-algorithm>' # RS256, HS256, etc.
IAM_ENCRYPTION_ALGORITHM='<your-algorithm>' # RS256, HS256, JWKS, etc.
IAM_ENCRYPTION_KEY='<your-public-key>' # public key
IAM_ENCRYPTION_KEY_PATH='<your-public-key-path>' # public key path
IAM_VERSION='<your-version-keycloak>' # Keycloak version
Expand Down
131 changes: 131 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Security Features

## JWKS Endpoint URL Validation

### Overview

Per prevenire attacchi SSRF (Server-Side Request Forgery), il bundle implementa una validazione rigorosa dell'URL del JWKS endpoint.

### Validazione dell'URL Base

L'URL base di Keycloak (`base_url`) viene validato nel costruttore di `JWKSTokenDecoder` per garantire:

1. **Formato URL valido**: L'URL deve avere uno schema (scheme) e un host validi
2. **Solo HTTPS**: Viene forzato l'uso di HTTPS per ambienti non-localhost
3. **Blocco IP privati**: Gli indirizzi IP privati (RFC 1918) sono bloccati automaticamente
4. **Blocco endpoint metadata**: Gli endpoint di metadata cloud (es. 169.254.169.254) sono bloccati
5. **Localhost consentito**: HTTP è consentito solo per localhost (127.0.0.1, ::1, localhost)

### Whitelist Domini JWKS

È possibile configurare una whitelist di domini autorizzati per le richieste JWKS:

```yaml
# config/packages/mainick_keycloak_client.yaml

mainick_keycloak_client:
keycloak:
base_url: '%env(IAM_BASE_URL)%'
realm: '%env(IAM_REALM)%'
# ... altre configurazioni ...

# Whitelist di domini consentiti per l'endpoint JWKS
allowed_jwks_domains:
- 'keycloak.example.com'
- '*.auth.example.com' # Supporta wildcard per sottodomini
```

### Comportamento Predefinito

Se non viene specificata alcuna whitelist (`allowed_jwks_domains`), il bundle consente **solo** il dominio presente in `base_url`.

Esempio:
- Se `base_url` è `https://keycloak.example.com`, solo questo dominio sarà consentito per le richieste JWKS
- Qualsiasi tentativo di reindirizzamento o richiesta a un dominio diverso verrà bloccato

### Wildcard per Sottodomini

È possibile utilizzare wildcard per consentire tutti i sottodomini di un dominio specifico:

```yaml
allowed_jwks_domains:
- '*.example.com' # Consente auth.example.com, keycloak.example.com, ecc.
```

**Nota**: Il wildcard `*.example.com` consente sia `auth.example.com` che `example.com` stesso.

### Validazione HTTPS

Per gli ambienti non-localhost, l'endpoint JWKS **deve** utilizzare HTTPS. Qualsiasi tentativo di utilizzare HTTP per domini pubblici verrà rifiutato con un'eccezione.

### Eccezioni di Sicurezza

Quando viene rilevata una violazione di sicurezza, viene lanciata un'eccezione `TokenDecoderException` con un messaggio dettagliato che indica:

- Il dominio non autorizzato
- Il motivo del rifiuto (non nella whitelist, IP privato, ecc.)

Esempio di messaggio di errore:
```
Invalid token: JWKS URL host "malicious.com" is not in the allowed domains whitelist
```

### Hosts Bloccati

I seguenti pattern di host sono automaticamente bloccati:

- Indirizzi IP privati (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
- Indirizzi IP riservati (169.254.0.0/16, 224.0.0.0/4, 240.0.0.0/4)
- Endpoint metadata cloud:
- `metadata.google.internal`
- `169.254.169.254` (AWS metadata)
- Qualsiasi host contenente `metadata` o `internal`

### Best Practices

1. **Ambiente di produzione**: Specificare sempre una whitelist esplicita di domini autorizzati
2. **HTTPS obbligatorio**: Non utilizzare HTTP per domini pubblici
3. **Minimizzare la whitelist**: Includere solo i domini strettamente necessari
4. **Evitare wildcard ampi**: Preferire domini specifici quando possibile
5. **Monitoraggio**: Registrare e monitorare le eccezioni `TokenDecoderException` per rilevare potenziali tentativi di attacco

### Esempio di Configurazione Sicura

```yaml
mainick_keycloak_client:
keycloak:
verify_ssl: true
base_url: 'https://keycloak.example.com'
realm: 'production'
client_id: '%env(IAM_CLIENT_ID)%'
client_secret: '%env(IAM_CLIENT_SECRET)%'
# ... altre configurazioni ...

# Whitelist rigorosa per l'ambiente di produzione
allowed_jwks_domains:
- 'keycloak.example.com'
- 'auth.example.com'
```

### Test di Sicurezza

Il bundle include test completi per verificare:

- Validazione del formato URL
- Rifiuto di HTTP per domini non-localhost
- Blocco di IP privati
- Blocco di endpoint metadata
- Funzionamento della whitelist domini
- Supporto wildcard per sottodomini
- Validazione HTTPS per endpoint JWKS

Per eseguire i test di sicurezza:

```bash
./vendor/bin/phpunit tests/Token/JWKSTokenDecoderTest.php
```

## Segnalazione Vulnerabilità

Se scopri una vulnerabilità di sicurezza, ti preghiamo di **NON** aprire un issue pubblico. Invia invece una segnalazione privata seguendo le linee guida nel file `SECURITY.md` nella root del progetto.

4 changes: 4 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('encryption_key_path')->defaultNull()->end()
->scalarNode('encryption_key_passphrase')->defaultNull()->end()
->scalarNode('version')->defaultNull()->end()
->arrayNode('allowed_jwks_domains')
->info('Whitelist of allowed domains for JWKS endpoint requests. If empty, only the base_url domain is allowed.')
->scalarPrototype()->end()
->end()
->end()
->validate()
->ifTrue(function ($v) {
Expand Down
25 changes: 25 additions & 0 deletions src/Exception/TokenDecoderException.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,29 @@ public static function forAudienceMismatch(\Exception $e): self
{
return new self('Audience mismatch', $e);
}

public static function forInvalidToken(\Exception $e): self
{
return new self('Invalid token', $e);
}

public static function forInvalidConfiguration(string $message, ?\Exception $e = null): self
{
return new self($message, $e ?? new \Exception($message));
}

public static function forJwksError(string $message, \Exception $e): self
{
return new self('JWKS error: ' . $message, $e);
}

public static function forDecodingError(string $message, \Exception $e): self
{
return new self('Token decoding error: ' . $message, $e);
}

public static function forSecurityViolation(string $message, ?\Exception $e = null): self
{
return new self('Security violation: ' . $message, $e ?? new \Exception($message));
}
}
8 changes: 5 additions & 3 deletions src/Provider/KeycloakClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
class KeycloakClient implements IamClientInterface
{
private Keycloak $keycloakProvider;
private ClientInterface $httpClient;

public function __construct(
private readonly LoggerInterface $keycloakClientLogger,
Expand Down Expand Up @@ -57,14 +58,15 @@ public function __construct(
$this->keycloakProvider->setVersion($this->version);
}

$httpClient = new Client([
$this->httpClient = new Client([
'verify' => $this->verify_ssl,
]);
$this->keycloakProvider->setHttpClient($httpClient);
$this->keycloakProvider->setHttpClient($this->httpClient);
}

public function setHttpClient(ClientInterface $httpClient): void
{
$this->httpClient = $httpClient;
$this->keycloakProvider->setHttpClient($httpClient);
}

Expand Down Expand Up @@ -101,7 +103,7 @@ public function verifyToken(AccessTokenInterface $token): ?UserRepresentationDTO
'values' => $token->getValues(),
]);

$decoder = TokenDecoderFactory::create($this->encryption_algorithm);
$decoder = TokenDecoderFactory::create($this->encryption_algorithm, ['base_url' => $this->base_url, 'realm' => $this->realm], $this->httpClient);
$tokenDecoded = $decoder->decode($accessToken->getToken(), $this->encryption_key);
$decoder->validateToken($this->realm, $tokenDecoded);
$this->keycloakClientLogger->info('KeycloakClient::verifyToken', [
Expand Down
Loading