Skip to content

Commit

Permalink
Add WWW-Authenticate HTTP response header to token-related problem ty…
Browse files Browse the repository at this point in the history
…pes (#127)

Fixes #126
  • Loading branch information
jpraet authored Oct 23, 2024
1 parent 18b2613 commit e47e441
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.github.belgif.rest.problem;

import java.net.URI;
import java.util.Collections;
import java.util.Map;

import io.github.belgif.rest.problem.api.ClientProblem;
import io.github.belgif.rest.problem.api.HttpResponseHeaders;
import io.github.belgif.rest.problem.api.ProblemType;

/**
Expand All @@ -12,7 +15,7 @@
* https://www.belgif.be/specification/rest/api-guide/#expired-access-token</a>
*/
@ProblemType(ExpiredAccessTokenProblem.TYPE)
public class ExpiredAccessTokenProblem extends ClientProblem {
public class ExpiredAccessTokenProblem extends ClientProblem implements HttpResponseHeaders {

/**
* The problem type.
Expand Down Expand Up @@ -52,4 +55,10 @@ public ExpiredAccessTokenProblem() {
setDetail(DETAIL);
}

@Override
public Map<String, Object> getHttpResponseHeaders() {
return Collections.singletonMap(WWW_AUTHENTICATE,
"Bearer error=\"invalid_token\", error_description=\"The access token expired\"");
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package io.github.belgif.rest.problem;

import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;

import io.github.belgif.rest.problem.api.ClientProblem;
import io.github.belgif.rest.problem.api.HttpResponseHeaders;
import io.github.belgif.rest.problem.api.ProblemType;

/**
Expand All @@ -12,7 +16,7 @@
* https://www.belgif.be/specification/rest/api-guide/#invalid-access-token</a>
*/
@ProblemType(InvalidAccessTokenProblem.TYPE)
public class InvalidAccessTokenProblem extends ClientProblem {
public class InvalidAccessTokenProblem extends ClientProblem implements HttpResponseHeaders {

/**
* The problem type.
Expand Down Expand Up @@ -47,9 +51,46 @@ public class InvalidAccessTokenProblem extends ClientProblem {

private static final long serialVersionUID = 1L;

private final String reason;

public InvalidAccessTokenProblem() {
this(DETAIL, "The access token is invalid");
}

public InvalidAccessTokenProblem(String reason) {
this(DETAIL + ": " + reason, reason);
}

private InvalidAccessTokenProblem(String detail, String reason) {
super(TYPE_URI, HREF, TITLE, STATUS);
setDetail(DETAIL);
setDetail(detail);
this.reason = reason;
}

@Override
public Map<String, Object> getHttpResponseHeaders() {
return Collections.singletonMap(WWW_AUTHENTICATE,
String.format("Bearer error=\"invalid_token\", error_description=\"%s\"", reason));
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
InvalidAccessTokenProblem that = (InvalidAccessTokenProblem) o;
return Objects.equals(reason, that.reason);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), reason);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonSetter;

import io.github.belgif.rest.problem.api.ClientProblem;
import io.github.belgif.rest.problem.api.HttpResponseHeaders;
import io.github.belgif.rest.problem.api.ProblemType;

/**
Expand All @@ -19,7 +21,7 @@
* https://www.belgif.be/specification/rest/api-guide/#missing-scope</a>
*/
@ProblemType(MissingScopeProblem.TYPE)
public class MissingScopeProblem extends ClientProblem {
public class MissingScopeProblem extends ClientProblem implements HttpResponseHeaders {

/**
* The problem type.
Expand Down Expand Up @@ -99,4 +101,9 @@ public int hashCode() {
return Objects.hash(super.hashCode(), requiredScopes);
}

@Override
public Map<String, Object> getHttpResponseHeaders() {
return Collections.singletonMap(WWW_AUTHENTICATE, "Bearer error=\"insufficient_scope\"");
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.github.belgif.rest.problem;

import java.net.URI;
import java.util.Collections;
import java.util.Map;

import io.github.belgif.rest.problem.api.ClientProblem;
import io.github.belgif.rest.problem.api.HttpResponseHeaders;
import io.github.belgif.rest.problem.api.ProblemType;

/**
Expand All @@ -12,7 +15,7 @@
* https://www.belgif.be/specification/rest/api-guide/#no-access-token</a>
*/
@ProblemType(NoAccessTokenProblem.TYPE)
public class NoAccessTokenProblem extends ClientProblem {
public class NoAccessTokenProblem extends ClientProblem implements HttpResponseHeaders {

/**
* The problem type.
Expand Down Expand Up @@ -52,4 +55,9 @@ public NoAccessTokenProblem() {
setDetail(DETAIL);
}

@Override
public Map<String, Object> getHttpResponseHeaders() {
return Collections.singletonMap(WWW_AUTHENTICATE, "Bearer");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
*/
public interface HttpResponseHeaders {

/**
* The Retry-After HTTP header.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After">Retry-After</a>
*/
String RETRY_AFTER = "Retry-After";

/**
* The WWW-Authenticate HTTP header.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate">WWW-Authenticate</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc6750#section-3">rfc6750#section-3</a>
*/
String WWW_AUTHENTICATE = "WWW-Authenticate";

/**
* Return the HTTP response headers.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@
*/
public interface RetryAfter extends HttpResponseHeaders {

/**
* The Retry-After HTTP header.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After">Retry-After</a>
*/
String RETRY_AFTER = "Retry-After";

OffsetDateTime getRetryAfter();

Long getRetryAfterSec();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import org.junit.jupiter.api.Test;

import io.github.belgif.rest.problem.api.HttpResponseHeaders;
import io.github.belgif.rest.problem.api.ProblemType;

class ExpiredAccessTokenProblemTest {
Expand All @@ -18,6 +19,8 @@ void construct() {
assertThat(problem.getStatus()).isEqualTo(401);
assertThat(problem.getDetail())
.isEqualTo("The Bearer access token found in the Authorization HTTP header has expired");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer error=\"invalid_token\", error_description=\"The access token expired\"");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import org.junit.jupiter.api.Test;

import io.github.belgif.rest.problem.api.HttpResponseHeaders;
import io.github.belgif.rest.problem.api.ProblemType;

class InvalidAccessTokenProblemTest {
Expand All @@ -18,6 +19,22 @@ void construct() {
assertThat(problem.getStatus()).isEqualTo(401);
assertThat(problem.getDetail())
.isEqualTo("The Bearer access token found in the Authorization HTTP header is invalid");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer error=\"invalid_token\", error_description=\"The access token is invalid\"");
}

@Test
void customReason() {
InvalidAccessTokenProblem problem = new InvalidAccessTokenProblem("Custom reason");
assertThat(problem.getType()).hasToString("urn:problem-type:belgif:invalidAccessToken");
assertThat(problem.getHref())
.hasToString("https://www.belgif.be/specification/rest/api-guide/problems/invalidAccessToken.html");
assertThat(problem.getTitle()).isEqualTo("Invalid Access Token");
assertThat(problem.getStatus()).isEqualTo(401);
assertThat(problem.getDetail())
.isEqualTo("The Bearer access token found in the Authorization HTTP header is invalid: Custom reason");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer error=\"invalid_token\", error_description=\"Custom reason\"");
}

@Test
Expand All @@ -27,4 +44,23 @@ void problemTypeAnnotation() {
.isEqualTo("urn:problem-type:belgif:invalidAccessToken");
}

@Test
void equalsAndHashCode() {
InvalidAccessTokenProblem problem = new InvalidAccessTokenProblem("A");
InvalidAccessTokenProblem equal = new InvalidAccessTokenProblem("A");
InvalidAccessTokenProblem other = new InvalidAccessTokenProblem("B");
InvalidAccessTokenProblem otherBis = new InvalidAccessTokenProblem();
otherBis.setDetail("other");

assertThat(problem).isEqualTo(problem);
assertThat(problem).hasSameHashCodeAs(problem);
assertThat(problem).isEqualTo(equal);
assertThat(problem).hasSameHashCodeAs(equal);
assertThat(problem).hasToString(equal.toString());
assertThat(problem).isNotEqualTo(other);
assertThat(problem).isNotEqualTo(otherBis);
assertThat(problem).doesNotHaveSameHashCodeAs(other);
assertThat(problem).isNotEqualTo("other type");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import org.junit.jupiter.api.Test;

import io.github.belgif.rest.problem.api.HttpResponseHeaders;
import io.github.belgif.rest.problem.api.ProblemType;

class MissingScopeProblemTest {
Expand All @@ -18,6 +19,8 @@ void construct() {
.hasToString("https://www.belgif.be/specification/rest/api-guide/problems/missingScope.html");
assertThat(problem.getTitle()).isEqualTo("Missing Scope");
assertThat(problem.getStatus()).isEqualTo(403);
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer error=\"insufficient_scope\"");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import org.junit.jupiter.api.Test;

import io.github.belgif.rest.problem.api.HttpResponseHeaders;
import io.github.belgif.rest.problem.api.ProblemType;

class NoAccessTokenProblemTest {
Expand All @@ -18,6 +19,7 @@ void construct() {
assertThat(problem.getStatus()).isEqualTo(401);
assertThat(problem.getDetail())
.isEqualTo("No Bearer access token found in Authorization HTTP header");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE, "Bearer");
}

@Test
Expand Down
2 changes: 2 additions & 0 deletions src/main/asciidoc/release-notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*belgif-rest-problem-bom:*

* Added Maven BOM (Bill of Materials) for dependency versions of belgif-rest-problem modules
* Ensure ProblemClientResponseFilter gets registered on JAX-RS Client
* Add https://www.rfc-editor.org/rfc/rfc6750#section-3[WWW-Authenticate] HTTP response header to token-related problem types

*belgif-rest-problem-java-ee:*

Expand Down

0 comments on commit e47e441

Please sign in to comment.