Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve WWW-Authenticate headers #148

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -50,15 +51,48 @@ public class ExpiredAccessTokenProblem extends ClientProblem implements HttpResp

private static final long serialVersionUID = 1L;

private String realm;

public ExpiredAccessTokenProblem() {
super(TYPE_URI, HREF, TITLE, STATUS);
setDetail(DETAIL);
}

public ExpiredAccessTokenProblem(String realm) {
this();
this.realm = realm;
}

public ExpiredAccessTokenProblem realm(String realm) {
this.realm = realm;
return this;
}

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

@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;
}
ExpiredAccessTokenProblem that = (ExpiredAccessTokenProblem) o;
return Objects.equals(realm, that.realm);
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public class InvalidAccessTokenProblem extends ClientProblem implements HttpResp

private final String reason;

private String realm;

public InvalidAccessTokenProblem() {
this(DETAIL, "The access token is invalid");
}
Expand All @@ -67,10 +69,16 @@ private InvalidAccessTokenProblem(String detail, String reason) {
this.reason = reason;
}

public InvalidAccessTokenProblem realm(String realm) {
this.realm = realm;
return this;
}

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

@Override
Expand All @@ -85,12 +93,12 @@ public boolean equals(Object o) {
return false;
}
InvalidAccessTokenProblem that = (InvalidAccessTokenProblem) o;
return Objects.equals(reason, that.reason);
return Objects.equals(reason, that.reason) && Objects.equals(realm, that.realm);
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public class MissingScopeProblem extends ClientProblem implements HttpResponseHe

private final List<String> requiredScopes = new ArrayList<>();

private String realm;

public MissingScopeProblem() {
super(TYPE_URI, HREF, TITLE, STATUS);
}
Expand All @@ -62,6 +64,11 @@ public MissingScopeProblem(String... requiredScopes) {
setRequiredScopes(requiredScopes);
}

public MissingScopeProblem realm(String realm) {
this.realm = realm;
return this;
}

public List<String> getRequiredScopes() {
return Collections.unmodifiableList(requiredScopes);
}
Expand Down Expand Up @@ -93,17 +100,21 @@ public boolean equals(Object o) {
return false;
}
MissingScopeProblem that = (MissingScopeProblem) o;
return Objects.equals(requiredScopes, that.requiredScopes);
return Objects.equals(requiredScopes, that.requiredScopes) && Objects.equals(realm, that.realm);
}

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

@Override
public Map<String, Object> getHttpResponseHeaders() {
return Collections.singletonMap(WWW_AUTHENTICATE, "Bearer error=\"insufficient_scope\"");
return Collections.singletonMap(WWW_AUTHENTICATE,
"Bearer " + (realm != null ? "realm=\"" + realm + "\", " : "")
+ "error=\"insufficient_scope\", error_description=\"Missing scope to perform request\""
+ (!requiredScopes.isEmpty()
? ", scope=\"" + String.join(" ", requiredScopes) + "\"" : ""));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -50,14 +51,47 @@ public class NoAccessTokenProblem extends ClientProblem implements HttpResponseH

private static final long serialVersionUID = 1L;

private String realm;

public NoAccessTokenProblem() {
super(TYPE_URI, HREF, TITLE, STATUS);
setDetail(DETAIL);
}

public NoAccessTokenProblem(String realm) {
this();
this.realm = realm;
}

public NoAccessTokenProblem realm(String realm) {
this.realm = realm;
return this;
}

@Override
public Map<String, Object> getHttpResponseHeaders() {
return Collections.singletonMap(WWW_AUTHENTICATE, "Bearer");
return Collections.singletonMap(WWW_AUTHENTICATE,
"Bearer" + (realm != null ? " realm=\"" + realm + "\"" : ""));
}

@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;
}
NoAccessTokenProblem that = (NoAccessTokenProblem) o;
return Objects.equals(realm, that.realm);
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ void construct() {
"Bearer error=\"invalid_token\", error_description=\"The access token expired\"");
}

@Test
void constructWithRealm() {
ExpiredAccessTokenProblem problem = new ExpiredAccessTokenProblem("test");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer realm=\"test\", error=\"invalid_token\", error_description=\"The access token expired\"");
}

@Test
void realm() {
ExpiredAccessTokenProblem problem = new ExpiredAccessTokenProblem().realm("test");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer realm=\"test\", error=\"invalid_token\", error_description=\"The access token expired\"");
}

@Test
void problemTypeAnnotation() {
assertThat(ExpiredAccessTokenProblem.class).hasAnnotation(ProblemType.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ void construct() {
"Bearer error=\"invalid_token\", error_description=\"The access token is invalid\"");
}

@Test
void realm() {
InvalidAccessTokenProblem problem = new InvalidAccessTokenProblem().realm("test");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer realm=\"test\", error=\"invalid_token\", error_description=\"The access token is invalid\"");
}

@Test
void customReason() {
InvalidAccessTokenProblem problem = new InvalidAccessTokenProblem("Custom reason");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,24 @@ void construct() {
assertThat(problem.getTitle()).isEqualTo("Missing Scope");
assertThat(problem.getStatus()).isEqualTo(403);
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer error=\"insufficient_scope\"");
"Bearer error=\"insufficient_scope\", error_description=\"Missing scope to perform request\"");
}

@Test
void realm() {
MissingScopeProblem problem = new MissingScopeProblem().realm("test");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer realm=\"test\", error=\"insufficient_scope\", "
+ "error_description=\"Missing scope to perform request\"");
}

@Test
void constructWithRequiredScopes() {
MissingScopeProblem problem = new MissingScopeProblem("required-scope");
assertThat(problem.getRequiredScopes()).isUnmodifiable().containsExactly("required-scope");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer error=\"insufficient_scope\", error_description=\"Missing scope to perform request\"" +
", scope=\"required-scope\"");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ void construct() {
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE, "Bearer");
}

@Test
void constructWithRealm() {
NoAccessTokenProblem problem = new NoAccessTokenProblem("test");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer realm=\"test\"");
}

@Test
void realm() {
NoAccessTokenProblem problem = new NoAccessTokenProblem().realm("test");
assertThat(problem.getHttpResponseHeaders()).containsEntry(HttpResponseHeaders.WWW_AUTHENTICATE,
"Bearer realm=\"test\"");
}

@Test
void problemTypeAnnotation() {
assertThat(NoAccessTokenProblem.class).hasAnnotation(ProblemType.class);
Expand Down
3 changes: 3 additions & 0 deletions src/main/asciidoc/release-notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
* Don't include null (issue) input values when serializing
* Replace `urn:problem-type:cbss:input-validation:referencedResourceNotFound`
by standardized `urn:problem-type:belgif:input-validation:referencedResourceNotFound`
* Improve WWW-Authenticate header for token-related problem types:
** Support setting the "realm" attribute
** Add "error_description" and "scope" attributes for missing_scope

*belgif-rest-problem-quarkus:*

Expand Down
Loading