feat: Add bearer token errors for rfc6750#section-3.1 (#1868)

pull/1869/head
guqing 2022-04-21 16:46:12 +08:00 committed by GitHub
parent c999c35a20
commit 599ab5618b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 439 additions and 0 deletions

View File

@ -0,0 +1,110 @@
package run.halo.app.identity.authentication.verifier;
import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.Assert;
/**
* A representation of a
* <a href="https://tools.ietf.org/html/rfc6750#section-3.1">Bearer Token Error</a>.
*
* @author guqing
* @see BearerTokenErrorCodes
* @see
* <a href="https://tools.ietf.org/html/rfc6750#section-3">RFC 6750 Section 3: The WWW-Authenticate Response Header Field</a>
* @see
* <a href="https://github.com/spring-projects/spring-security/blob/e79b6b3ac8/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java">oauth2 resource server BearerTokenError</a>
* @since 2.0.0
*/
public final class BearerTokenError extends OAuth2Error {
private final HttpStatus httpStatus;
private final String scope;
/**
* Create a {@code BearerTokenError} using the provided parameters
*
* @param errorCode the error code
* @param httpStatus the HTTP status
*/
public BearerTokenError(String errorCode, HttpStatus httpStatus, String description,
String errorUri) {
this(errorCode, httpStatus, description, errorUri, null);
}
/**
* Create a {@code BearerTokenError} using the provided parameters
*
* @param errorCode the error code
* @param httpStatus the HTTP status
* @param description the description
* @param errorUri the URI
* @param scope the scope
*/
public BearerTokenError(String errorCode, HttpStatus httpStatus, String description,
String errorUri,
String scope) {
super(errorCode, description, errorUri);
Assert.notNull(httpStatus, "httpStatus cannot be null");
Assert.isTrue(isDescriptionValid(description),
"description contains invalid ASCII characters, it must conform to RFC 6750");
Assert.isTrue(isErrorCodeValid(errorCode),
"errorCode contains invalid ASCII characters, it must conform to RFC 6750");
Assert.isTrue(isErrorUriValid(errorUri),
"errorUri contains invalid ASCII characters, it must conform to RFC 6750");
Assert.isTrue(isScopeValid(scope),
"scope contains invalid ASCII characters, it must conform to RFC 6750");
this.httpStatus = httpStatus;
this.scope = scope;
}
/**
* Return the HTTP status.
*
* @return the HTTP status
*/
public HttpStatus getHttpStatus() {
return this.httpStatus;
}
/**
* Return the scope.
*
* @return the scope
*/
public String getScope() {
return this.scope;
}
private static boolean isDescriptionValid(String description) {
return description == null || description.chars().allMatch((c) ->
withinTheRangeOf(c, 0x20, 0x21)
|| withinTheRangeOf(c, 0x23, 0x5B)
|| withinTheRangeOf(c, 0x5D, 0x7E));
}
private static boolean isErrorCodeValid(String errorCode) {
return errorCode.chars().allMatch((c) ->
withinTheRangeOf(c, 0x20, 0x21)
|| withinTheRangeOf(c, 0x23, 0x5B)
|| withinTheRangeOf(c, 0x5D, 0x7E));
}
private static boolean isErrorUriValid(String errorUri) {
return errorUri == null || errorUri.chars()
.allMatch(
(c) -> c == 0x21 || withinTheRangeOf(c, 0x23, 0x5B) || withinTheRangeOf(c, 0x5D,
0x7E));
}
private static boolean isScopeValid(String scope) {
return scope == null || scope.chars().allMatch((c) ->
withinTheRangeOf(c, 0x20, 0x21)
|| withinTheRangeOf(c, 0x23, 0x5B)
|| withinTheRangeOf(c, 0x5D, 0x7E));
}
private static boolean withinTheRangeOf(int c, int min, int max) {
return c >= min && c <= max;
}
}

View File

@ -0,0 +1,31 @@
package run.halo.app.identity.authentication.verifier;
/**
* Standard error codes defined by the OAuth 2.0 Authorization Framework: Bearer Token
* Usage.
*
* @author guqing
* @see
* <a href="https://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 Section 3.1: Error Codes</a>
* @since 2.0.0
*/
public interface BearerTokenErrorCodes {
/**
* {@code invalid_request} - The request is missing a required parameter, includes an
* unsupported parameter or parameter value, repeats the same parameter, uses more
* than one method for including an access token, or is otherwise malformed.
*/
String INVALID_REQUEST = "invalid_request";
/**
* {@code invalid_token} - The access token provided is expired, revoked, malformed,
* or invalid for other reasons.
*/
String INVALID_TOKEN = "invalid_token";
/**
* {@code insufficient_scope} - The request requires higher privileges than provided
* by the access token.
*/
String INSUFFICIENT_SCOPE = "insufficient_scope";
}

View File

@ -0,0 +1,79 @@
package run.halo.app.identity.authentication.verifier;
import org.springframework.http.HttpStatus;
/**
* A factory for creating {@link BearerTokenError} instances that correspond to the
* registered <a href="https://tools.ietf.org/html/rfc6750#section-3.1">Bearer Token Error Codes</a>
*
* @author guqing
* @since 2.0.0
*/
public class BearerTokenErrors {
private static final BearerTokenError DEFAULT_INVALID_REQUEST =
invalidRequest("Invalid request");
private static final BearerTokenError DEFAULT_INVALID_TOKEN = invalidToken("Invalid token");
private static final BearerTokenError DEFAULT_INSUFFICIENT_SCOPE =
insufficientScope("Insufficient scope", null);
private static final String DEFAULT_URI = "https://tools.ietf.org/html/rfc6750#section-3.1";
private BearerTokenErrors() {
}
/**
* Create a {@link BearerTokenError} caused by an invalid request
*
* @param message a description of the error
* @return a {@link BearerTokenError}
*/
public static BearerTokenError invalidRequest(String message) {
try {
return new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST,
HttpStatus.BAD_REQUEST, message,
DEFAULT_URI);
} catch (IllegalArgumentException ex) {
// some third-party library error messages are not suitable for RFC 6750's
// error message charset
return DEFAULT_INVALID_REQUEST;
}
}
/**
* Create a {@link BearerTokenError} caused by an invalid token
*
* @param message a description of the error
* @return a {@link BearerTokenError}
*/
public static BearerTokenError invalidToken(String message) {
try {
return new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN,
HttpStatus.UNAUTHORIZED, message,
DEFAULT_URI);
} catch (IllegalArgumentException ex) {
// some third-party library error messages are not suitable for RFC 6750's
// error message charset
return DEFAULT_INVALID_TOKEN;
}
}
/**
* Create a {@link BearerTokenError} caused by an invalid token
*
* @param scope the scope attribute to use in the error
* @return a {@link BearerTokenError}
*/
public static BearerTokenError insufficientScope(String message, String scope) {
try {
return new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE,
HttpStatus.FORBIDDEN, message,
DEFAULT_URI, scope);
} catch (IllegalArgumentException ex) {
// some third-party library error messages are not suitable for RFC 6750's
// error message charset
return DEFAULT_INSUFFICIENT_SCOPE;
}
}
}

View File

@ -0,0 +1,138 @@
package run.halo.app.authentication.verifyer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import run.halo.app.identity.authentication.verifier.BearerTokenError;
/**
* Tests for {@link BearerTokenError}
*
* @author guqing
* @see <a href="https://tools.ietf.org/html/rfc6750#section-3.1">Bearer Token Error</a>
* @since 2.0.0
*/
public class BearerTokenErrorTest {
private static final String TEST_ERROR_CODE = "test-code";
private static final HttpStatus TEST_HTTP_STATUS = HttpStatus.UNAUTHORIZED;
private static final String TEST_DESCRIPTION = "test-description";
private static final String TEST_URI = "https://example.com";
private static final String TEST_SCOPE = "test-scope";
@Test
public void constructorWithErrorCodeWhenErrorCodeIsValidThenCreated() {
BearerTokenError error =
new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, null, null);
assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE);
assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS);
assertThat(error.getDescription()).isNull();
assertThat(error.getUri()).isNull();
assertThat(error.getScope()).isNull();
}
@Test
public void constructorWithErrorCodeThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new BearerTokenError(null, TEST_HTTP_STATUS, null, null))
.withMessage("errorCode cannot be empty");
}
@Test
public void constructorWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new BearerTokenError("", TEST_HTTP_STATUS, null, null))
.withMessage("errorCode cannot be empty");
}
@Test
public void constructorWhenHttpStatusIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new BearerTokenError(TEST_ERROR_CODE, null, null, null))
.withMessage("httpStatus cannot be null");
}
@Test
public void constructorWithAllParametersWhenAllParametersAreValidThenCreated() {
BearerTokenError error =
new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI,
TEST_SCOPE);
assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE);
assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS);
assertThat(error.getDescription()).isEqualTo(TEST_DESCRIPTION);
assertThat(error.getUri()).isEqualTo(TEST_URI);
assertThat(error.getScope()).isEqualTo(TEST_SCOPE);
}
@Test
public void constructorWithAllParametersWhenErrorCodeIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(
() -> new BearerTokenError(null, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI,
TEST_SCOPE))
.withMessage("errorCode cannot be empty");
}
@Test
public void constructorWithAllParametersThrowIllegalArgumentException1() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new BearerTokenError("", TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI,
TEST_SCOPE))
.withMessage("errorCode cannot be empty");
}
@Test
public void constructorWithAllParametersThrowIllegalArgumentException2() {
assertThatIllegalArgumentException()
.isThrownBy(
() -> new BearerTokenError(TEST_ERROR_CODE, null, TEST_DESCRIPTION, TEST_URI,
TEST_SCOPE))
.withMessage("httpStatus cannot be null");
}
@Test
public void constructorWhenErrorCodeIsInvalidThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new BearerTokenError(TEST_ERROR_CODE + "\"",
TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE)
)
.withMessageContaining("errorCode")
.withMessageContaining("RFC 6750");
}
@Test
public void constructorWithAllParametersThrowIllegalArgumentException3() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS,
TEST_DESCRIPTION + "\"", TEST_URI, TEST_SCOPE)
)
.withMessageContaining("description")
.withMessageContaining("RFC 6750");
}
@Test
public void constructorWithAllParametersThrowIllegalArgumentException4() {
assertThatIllegalArgumentException()
.isThrownBy(
() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION,
TEST_URI + "\"", TEST_SCOPE)
)
.withMessageContaining("errorUri")
.withMessageContaining("RFC 6750");
}
@Test
public void constructorWithAllParametersWhenScopeIsInvalidThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS,
TEST_DESCRIPTION, TEST_URI, TEST_SCOPE + "\"")
)
.withMessageContaining("scope")
.withMessageContaining("RFC 6750");
}
}

View File

@ -0,0 +1,81 @@
package run.halo.app.authentication.verifyer;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import run.halo.app.identity.authentication.verifier.BearerTokenError;
import run.halo.app.identity.authentication.verifier.BearerTokenErrorCodes;
import run.halo.app.identity.authentication.verifier.BearerTokenErrors;
/**
* Tests for {@link BearerTokenErrors}
*
* @author guqing
* @since 2.0.0
*/
public class BearerTokenErrorsTest {
@Test
public void invalidRequestWhenMessageGivenThenBearerTokenErrorReturned() {
String message = "message";
BearerTokenError error = BearerTokenErrors.invalidRequest(message);
assertThat(error.getErrorCode()).isSameAs(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getDescription()).isSameAs(message);
assertThat(error.getHttpStatus()).isSameAs(HttpStatus.BAD_REQUEST);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
}
@Test
public void invalidRequestWhenInvalidMessageGivenThenDefaultBearerTokenErrorReturned() {
String message = "has \"invalid\" chars";
BearerTokenError error = BearerTokenErrors.invalidRequest(message);
assertThat(error.getErrorCode()).isSameAs(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getDescription()).isEqualTo("Invalid request");
assertThat(error.getHttpStatus()).isSameAs(HttpStatus.BAD_REQUEST);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
}
@Test
public void invalidTokenWhenMessageGivenThenBearerTokenErrorReturned() {
String message = "message";
BearerTokenError error = BearerTokenErrors.invalidToken(message);
assertThat(error.getErrorCode()).isSameAs(BearerTokenErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).isSameAs(message);
assertThat(error.getHttpStatus()).isSameAs(HttpStatus.UNAUTHORIZED);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
}
@Test
public void invalidTokenWhenInvalidMessageGivenThenDefaultBearerTokenErrorReturned() {
String message = "has \"invalid\" chars";
BearerTokenError error = BearerTokenErrors.invalidToken(message);
assertThat(error.getErrorCode()).isSameAs(BearerTokenErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).isEqualTo("Invalid token");
assertThat(error.getHttpStatus()).isSameAs(HttpStatus.UNAUTHORIZED);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
}
@Test
public void insufficientScopeWhenMessageGivenThenBearerTokenErrorReturned() {
String message = "message";
String scope = "scope";
BearerTokenError error = BearerTokenErrors.insufficientScope(message, scope);
assertThat(error.getErrorCode()).isSameAs(BearerTokenErrorCodes.INSUFFICIENT_SCOPE);
assertThat(error.getDescription()).isSameAs(message);
assertThat(error.getHttpStatus()).isSameAs(HttpStatus.FORBIDDEN);
assertThat(error.getScope()).isSameAs(scope);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
}
@Test
public void insufficientScopeWhenInvalidMessageGivenThenDefaultBearerTokenErrorReturned() {
String message = "has \"invalid\" chars";
BearerTokenError error = BearerTokenErrors.insufficientScope(message, "scope");
assertThat(error.getErrorCode()).isSameAs(BearerTokenErrorCodes.INSUFFICIENT_SCOPE);
assertThat(error.getDescription()).isSameAs("Insufficient scope");
assertThat(error.getHttpStatus()).isSameAs(HttpStatus.FORBIDDEN);
assertThat(error.getScope()).isNull();
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
}
}