mirror of https://github.com/halo-dev/halo
feat: Add bearer token errors for rfc6750#section-3.1 (#1868)
parent
c999c35a20
commit
599ab5618b
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue