Skip to content

Commit

Permalink
Merge pull request #1589 from zalando/381_extract_subject_claim_from_…
Browse files Browse the repository at this point in the history
…jwt_token

Extract subject claim from JWT token
  • Loading branch information
msdousti authored Sep 18, 2023
2 parents 8ad19a3 + ad5b01e commit 60314c2
Show file tree
Hide file tree
Showing 52 changed files with 1,789 additions and 73 deletions.
109 changes: 87 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,63 @@ Logbook comes with some built-in strategies:
- [`StatusAtLeastStrategy`](logbook-core/src/main/java/org/zalando/logbook/core/StatusAtLeastStrategy.java)
- [`WithoutBodyStrategy`](logbook-core/src/main/java/org/zalando/logbook/core/WithoutBodyStrategy.java)

### Attribute Extractor
Starting with version 3.4.0, Logbook is equipped with a feature called *Attribute Extractor*. Attributes are basically a
list of key/value pairs that can be extracted from request and/or response, and logged with them. The idea was sprouted
from [issue 381](https://github.com/zalando/logbook/issues/381), where a feature was requested to extract the subject
claim from JWT tokens in the authorization header.

The `AttributeExtractor` interface has two `extract` methods: One that can extract attributes from the request only, and
one that has both request and response at its avail. The both return an instance of the `HttpAttributes` class, which is
basically a fancy `Map<String, Object>`. Notice that since the map values are of type `Object`, they should have a
proper `toString()` method in order for them to appear in the logs in a meaningful way. Alternatively, log formatters
can work around this by implementing their own serialization logic. For instance, the built-in log formatter
`JsonHttpLogFormatter` uses `ObjectMapper` to serialize the values.

Here is an example:

```java
final class OriginExtractor implements AttributeExtractor {

@Override
public HttpAttributes extract(final HttpRequest request) {
return HttpAttributes.of("origin", request.getOrigin());
}

}
```

Logbook must then be created by registering this attribute extractor:

```java
final Logbook logbook = Logbook.builder()
.attributeExtractor(new OriginExtractor())
.build();
```

This will result in request logs to include something like:
```text
"attributes":{"origin":"LOCAL"}
```

For more advanced examples, look at the `JwtFirstMatchingClaimExtractor` and `JwtAllMatchingClaimsExtractor` classes.
The former extracts the first claim matching a list of claim names from the request JWT token.
The latter extracts all claims matching a list of claim names from the request JWT token.

If you require to incorporate multiple `AttributeExtractor`s, you can use the class `CompositeAttributeExtractor`:

```java
final List<AttributeExtractor> extractors = List.of(
extractor1,
extractor2,
extractor3
);

final Logbook logbook = Logbook.builder()
.attributeExtractor(new CompositeAttributeExtractor(extractors))
.build();
```

### Phases

Logbook works in several different phases:
Expand Down Expand Up @@ -906,12 +963,13 @@ or the following table to see a list of possible integration points:
| `Logbook` | | Based on condition, filters, formatter and writer |
| `Predicate<HttpRequest>` | `requestCondition` | No filter; is later combined with `logbook.exclude` and `logbook.exclude` |
| `HeaderFilter` | | Based on `logbook.obfuscate.headers` |
| `PathFilter` | | Based on `logbook.obfuscate.paths` |
| `PathFilter` | | Based on `logbook.obfuscate.paths` |
| `QueryFilter` | | Based on `logbook.obfuscate.parameters` |
| `BodyFilter` | | `BodyFilters.defaultValue()`, see [filtering](#filtering) |
| `RequestFilter` | | `RequestFilters.defaultValue()`, see [filtering](#filtering) |
| `ResponseFilter` | | `ResponseFilters.defaultValue()`, see [filtering](#filtering) |
| `Strategy` | | `DefaultStrategy` |
| `AttributeExtractor` | | `NoOpAttributeExtractor` |
| `Sink` | | `DefaultSink` |
| `HttpLogFormatter` | | `JsonHttpLogFormatter` |
| `HttpLogWriter` | | `DefaultHttpLogWriter` |
Expand All @@ -933,27 +991,28 @@ MyClient(RestTemplateBuilder builder, LogbookClientHttpRequestInterceptor interc

#### Configuration

The following tables show the available configuration:

| Configuration | Description | Default |
|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `logbook.include` | Include only certain URLs (if defined) | `[]` |
| `logbook.exclude` | Exclude certain URLs (overrides `logbook.include`) | `[]` |
| `logbook.filter.enabled` | Enable the [`LogbookFilter`](#servlet) | `true` |
| `logbook.filter.form-request-mode` | Determines how [form requests](#form-requests) are handled | `body` |
| `logbook.secure-filter.enabled` | Enable the [`SecureLogbookFilter`](#servlet) | `true` |
| `logbook.format.style` | [Formatting style](#formatting) (`http`, `json`, `curl` or `splunk`) | `json` |
| `logbook.strategy` | [Strategy](#strategy) (`default`, `status-at-least`, `body-only-if-status-at-least`, `without-body`) | `default` |
| `logbook.minimum-status` | Minimum status to enable logging (`status-at-least` and `body-only-if-status-at-least`) | `400` |
| `logbook.obfuscate.headers` | List of header names that need obfuscation | `[Authorization]` |
| `logbook.obfuscate.paths` | List of paths that need obfuscation. Check [Filtering](#filtering) for syntax. | `[]` |
| `logbook.obfuscate.parameters` | List of parameter names that need obfuscation | `[access_token]` |
| `logbook.write.chunk-size` | Splits log lines into smaller chunks of size up-to `chunk-size`. | `0` (disabled) |
| `logbook.write.max-body-size` | Truncates the body up to `max-body-size` and appends `...`. | `-1` (disabled) |
| `logbook.httpclient.decompress-response` | Enables/disables additional decompression process for HttpClient with gzip encoded body (to logging purposes only). This means extra decompression and possible performance impact. | `false` (disabled) |
| `logbook.obfuscate.json-body-fields` | List of JSON body fields to be obfuscated | `[]` |
| `logbook.obfuscate.replacement` | A value to be used instead of an obfuscated one | `XXX` |
| `logbook.filters.body.default-enabled` | Enables/disables default body filters that are collected by java.util.ServiceLoader | `true` |
The following tables show the available configuration (sorted alphabetically):

| Configuration | Description | Default |
|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `logbook.attribute-extractors` | List of [AttributeExtractor](#attribute-extractor)s, including configurations such as `type` (currently `JwtFirstMatchingClaimExtractor` or `JwtAllMatchingClaimsExtractor`), `claim-names` and `claim-key`. | `[]` |
| `logbook.exclude` | Exclude certain URLs (overrides `logbook.include`) | `[]` |
| `logbook.filter.enabled` | Enable the [`LogbookFilter`](#servlet) | `true` |
| `logbook.filter.form-request-mode` | Determines how [form requests](#form-requests) are handled | `body` |
| `logbook.filters.body.default-enabled` | Enables/disables default body filters that are collected by java.util.ServiceLoader | `true` |
| `logbook.format.style` | [Formatting style](#formatting) (`http`, `json`, `curl` or `splunk`) | `json` |
| `logbook.httpclient.decompress-response` | Enables/disables additional decompression process for HttpClient with gzip encoded body (to logging purposes only). This means extra decompression and possible performance impact. | `false` (disabled) |
| `logbook.include` | Include only certain URLs (if defined) | `[]` |
| `logbook.minimum-status` | Minimum status to enable logging (`status-at-least` and `body-only-if-status-at-least`) | `400` |
| `logbook.obfuscate.headers` | List of header names that need obfuscation | `[Authorization]` |
| `logbook.obfuscate.json-body-fields` | List of JSON body fields to be obfuscated | `[]` |
| `logbook.obfuscate.parameters` | List of parameter names that need obfuscation | `[access_token]` |
| `logbook.obfuscate.paths` | List of paths that need obfuscation. Check [Filtering](#filtering) for syntax. | `[]` |
| `logbook.obfuscate.replacement` | A value to be used instead of an obfuscated one | `XXX` |
| `logbook.secure-filter.enabled` | Enable the [`SecureLogbookFilter`](#servlet) | `true` |
| `logbook.strategy` | [Strategy](#strategy) (`default`, `status-at-least`, `body-only-if-status-at-least`, `without-body`) | `default` |
| `logbook.write.chunk-size` | Splits log lines into smaller chunks of size up-to `chunk-size`. | `0` (disabled) |
| `logbook.write.max-body-size` | Truncates the body up to `max-body-size` and appends `...`. | `-1` (disabled) |

##### Example configuration

Expand All @@ -979,6 +1038,12 @@ logbook:
- password
write:
chunk-size: 1000
attribute-extractors:
- type: JwtFirstMatchingClaimExtractor
claim-names: [ "sub", "subject" ]
claim-key: Principal
- type: JwtAllMatchingClaimsExtractor
claim-names: [ "sub", "iat" ]
```
### logstash-logback-encoder
Expand Down
9 changes: 9 additions & 0 deletions logbook-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
<developerConnection>scm:git:[email protected]:zalando/logbook.git</developerConnection>
</scm>

<dependencies>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<version>3.15.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.zalando.logbook;

import org.apiguardian.api.API;
import org.zalando.logbook.attributes.HttpAttributes;

import java.io.IOException;
import java.util.Optional;
Expand Down Expand Up @@ -63,4 +64,8 @@ default HttpRequest withoutBody() {
return delegate().withoutBody();
}

@Override
default HttpAttributes getAttributes() {
return delegate().getAttributes();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.zalando.logbook;

import org.apiguardian.api.API;
import org.zalando.logbook.attributes.HttpAttributes;

import java.io.IOException;

Expand Down Expand Up @@ -31,4 +32,9 @@ default HttpResponse withoutBody() {
default String getReasonPhrase() {
return delegate().getReasonPhrase();
}

@Override
default HttpAttributes getAttributes() {
return delegate().getAttributes();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.zalando.logbook;

import org.apiguardian.api.API;
import org.zalando.logbook.attributes.HttpAttributes;

import java.io.IOException;
import java.util.Optional;
Expand Down Expand Up @@ -35,6 +36,10 @@ default String getRequestUri() {

String getQuery();

default HttpAttributes getAttributes() {
return HttpAttributes.EMPTY;
}

// TODO don't throw!
// TODO void vs pseudo-function (mutable)
HttpRequest withBody() throws IOException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.zalando.logbook;

import org.apiguardian.api.API;
import org.zalando.logbook.attributes.HttpAttributes;

import java.io.IOException;

Expand All @@ -16,6 +17,10 @@ public interface HttpResponse extends HttpMessage {

HttpResponse withoutBody();

default HttpAttributes getAttributes() {
return HttpAttributes.EMPTY;
}

default String getReasonPhrase() {
switch (getStatus()) {
// 1xx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.Singular;
import org.apiguardian.api.API;
import org.zalando.logbook.attributes.AttributeExtractor;

import javax.annotation.Nullable;
import java.util.List;
Expand All @@ -23,6 +24,7 @@ public static final class Builder {

}

@SuppressWarnings("unused")
@lombok.Builder(builderClassName = "Builder")
private static Logbook create(
@Nullable final Predicate<HttpRequest> condition,
Expand All @@ -34,6 +36,7 @@ private static Logbook create(
@Singular final List<RequestFilter> requestFilters,
@Singular final List<ResponseFilter> responseFilters,
@Nullable final Strategy strategy,
@Nullable final AttributeExtractor attributeExtractor,
@Nullable final Sink sink) {

@Nullable final QueryFilter queryFilter = queryFilters.stream()
Expand Down Expand Up @@ -72,6 +75,7 @@ private static Logbook create(
requestFilter,
responseFilter,
strategy,
attributeExtractor,
sink);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.zalando.logbook;

import org.apiguardian.api.API;
import org.zalando.logbook.attributes.AttributeExtractor;

import javax.annotation.Nullable;
import java.util.Comparator;
Expand Down Expand Up @@ -31,6 +32,7 @@ Logbook create(
@Nullable final RequestFilter requestFilter,
@Nullable final ResponseFilter responseFilter,
@Nullable final Strategy strategy,
@Nullable final AttributeExtractor attributeExtractor,
@Nullable final Sink sink);

}
4 changes: 2 additions & 2 deletions logbook-api/src/main/java/org/zalando/logbook/Strategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
* <p>
* At each of those points in time different options are available, e.g. to defer logging, apply conditions or even
* modify something.
*
* <a href="https://en.wikipedia.org/wiki/Strategy_pattern">Strategy pattern</a>
* <p>
* See <a href="https://en.wikipedia.org/wiki/Strategy_pattern">Strategy Pattern</a>.
*/
@API(status = STABLE)
public interface Strategy {
Expand Down
Loading

0 comments on commit 60314c2

Please sign in to comment.