Security
The REST expanders provide authentication and authorization stubs which can be used to implement these concepts depending on the needs of the project. This can be done using some expanded infrastructure through injected features or they can be implemented custom by a developer.
Some default implementations for authentication and authorization can be enabled using model options. These mainly provide integration with the account
base component. For obvious security reasons, by default all access into the API is blocked, so an implementation is needed to provide access in some way.
Authentication
Authentication is the step which validates user credentials and builds the UserContext
object for the application.
The AuthenticationProvider
class is generated as a stub in every Component which provides a REST API. By default an anonymous UserContext
is created in this stub and added to the Context
. This method takes in the Authorization
header, which is typically used for access control through HTTP. If the header has an invalid format, a MalformedHeaderException
is thrown.
Authentication can be implemented for the parsed Authorization
header in the filter-authorization-header
anchor, or if something other than the Authorization
is not used, the filter
anchors can be implemented instead.
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
ApiComponentUserContext userContext = ApiComponentUserContext.anonymous();
final String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null) {
int separatorPosition = authorizationHeader.indexOf(' ');
if (separatorPosition == -1) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must have format '<key> <credentials>'.");
}
String authorizationKey = authorizationHeader.substring(0, separatorPosition);
String authorizationCredentials = authorizationHeader.substring(separatorPosition + 1);
if (authorizationKey.isEmpty() || authorizationCredentials.isEmpty()) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must not contain empty key or credentials values.");
}
// @anchor:filter-authorization-header:start
// @anchor:filter-authorization-header:end
// anchor:custom-filter-authorization-header:start
// anchor:custom-filter-authorization-header:end
}
// @anchor:filter:start
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end
JaxNsContext.updateContext(requestContext, userContext);
}
Anonymous access
Unless there is a very good reason to do so, never do this on any application that publicly accessible.
Though not recommended, the most basic form of access to an API is anonymous access. This simply means that the API will have absolutely no user authentication and will allow all incoming calls.
Basic authentication
If you are using the account
base-component, you can enable basic HTTP authentication with the rest-expanders.
Component
Available with the
rest-account-security-expanders
bundle.
Adds (stateless) basic authentication to the JAX-RS REST API, which uses the account
base component to authenticate users where they can be authenticated using a username/password combination.
<options>
<jaxrs.auth.basic.enable/>
</options>
This will add a feature to the AuthenticationProvider
, which processes the Authorization
header if its value is formatted as Basic <credentials>
. The credentials in basic authentication are encoded with a base64 encoding. The username and password that are extracted are passed to the account
component, which performs authentication and returns a UserContext
.
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
MyComponentUserContext userContext = MyComponentUserContext.anonymous();
final String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null) {
int separatorPosition = authorizationHeader.indexOf(' ');
if (separatorPosition == -1) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must have format '<key> <credentials>'.");
}
String authorizationKey = authorizationHeader.substring(0, separatorPosition);
String authorizationCredentials = authorizationHeader.substring(separatorPosition + 1);
if (authorizationKey.isEmpty() || authorizationCredentials.isEmpty()) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must not contain empty key or credentials values.");
}
// @anchor:filter-authorization-header:start
if (authorizationKey.equals("Basic")) {
final String credentials = new String(DatatypeConverter.parseBase64Binary(authorizationCredentials),
StandardCharsets.US_ASCII);
final int colonPos = credentials.indexOf(":");
if (colonPos == -1) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Failed to parse basic authentication credentials.");
}
final String username = credentials.substring(0, colonPos);
final String password = credentials.substring(colonPos + 1);
if (username.isEmpty() || password.isEmpty()) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Basic authentication credentials are malformed.");
}
final UserInput userInput = new UserInput();
userInput.setName(username);
userInput.setPassword(password);
String paramString = "";
// anchor:custom-before-basic-authenticate:start
// anchor:custom-before-basic-authenticate:end
final AuthenticationAgent authenticationAgent = AuthenticationAgent.getAuthenticationAgent(Context.emptyContext());
final TaskResult<UserContext> authenticationResult = authenticationAgent.perform(userInput, paramString);
if (authenticationResult.isSuccess()) {
userContext.update(authenticationResult.getValue());
}
}
// @anchor:filter-authorization-header:end
// anchor:custom-filter-authorization-header:start
// anchor:custom-filter-authorization-header:end
}
// @anchor:filter:start
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end
JaxNsContext.updateContext(requestContext, userContext);
}
OpenID Connect authentication
If you are using the account
base-component with the option baseComponents.authentication.enableOIDC
, you can enable OpenID Connect authentication with the rest-expanders.
Component
Available with the
rest-account-security-expanders
bundle.
This option enables OpenID Connect authentication to the REST API, which authenticates against the users in the account
component.
Unlike with the base-components, only one OpenID Connect provider can be used currently, so the option value should be the identifier of the provider you have configured in the account component.
<options>
<jaxrs.auth.oidc.enable>google</jaxrs.auth.oidc.enable>
</options>
<options>
<jaxrs.auth.oidc.enable>keycloak</jaxrs.auth.oidc.enable>
</options>
The baseComponents.authentication.enableOIDC
option must be present, but the provider used for the rest-expander does not necessarily have to be enabled in general for the base-components, though of course there must always be matching configuration for the provider in the account.ns.properties
file.
In the AuthenticationProvider
code is added which processes the Authorization
header if its value is formatted as Bearer <accessToken>
. As indicated, this takes in an OAuth2 access token as a bearer token from the REST API.
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
MyComponentUserContext userContext = MyComponentUserContext.anonymous();
final String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null) {
int separatorPosition = authorizationHeader.indexOf(' ');
if (separatorPosition == -1) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must have format '<key> <credentials>'.");
}
String authorizationKey = authorizationHeader.substring(0, separatorPosition);
String authorizationCredentials = authorizationHeader.substring(separatorPosition + 1);
if (authorizationKey.isEmpty() || authorizationCredentials.isEmpty()) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must not contain empty key or credentials values.");
}
// @anchor:filter-authorization-header:start
if (authorizationKey.equals("Bearer")) {
final OIDCAccessInput accessInput = new OIDCAccessInput();
accessInput.setProvider("google");
accessInput.setAccessToken(authorizationCredentials);
String paramString = "";
// anchor:custom-before-oidc-authenticate:start
// anchor:custom-before-oidc-authenticate:end
final UserAgent userAgent = UserAgent.getUserAgent(Context.emptyContext());
final AuthenticationResult authenticationResult = userAgent.authenticate(AuthenticationDetails.from(accessInput));
if (authenticationResult.isSuccess()) {
userContext.update(authenticationResult.getUserContext());
}
}
// @anchor:filter-authorization-header:end
// anchor:custom-filter-authorization-header:start
// anchor:custom-filter-authorization-header:end
}
// @anchor:filter:start
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end
JaxNsContext.updateContext(requestContext, userContext);
}
Authorization
Authorization in the JAX-RS stack is handled by deciding if a call is allowed to proceed to the API based on the UserContext
. Authorization is handled in the AuthorizationProvider
class, which is executed after the AuthenticationProvider
that creates the UserContext
.
By default all access to the REST API is disabled. The reason for this approach is to prevent potential security issues if an API is left exposed by accident. This is achieved by preventing any access to the API for an anonymous UserContext
. It is possible to enable anonymous access if needed, but it is not recommended for use outside of development.
Calls with the OPTIONS
method are always permitted given that it does not expose any data, only information about the API itself.
Implementation
Authorization can be implemented in the filter
anchor of the provider. This is intended for fully custom authorization implementations. If authorization passes the method should return, whereas for unauthorized users, the NotAuthorizedException
should be thrown.
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
Context context = JaxNsContext.getContext(requestContext);
ApiComponentUserContext userContext = ApiComponentUserContext.from(context);
String path = requestContext.getUriInfo().getPath();
// @anchor:before-filter:start
// @anchor:before-filter:end
// anchor:custom-before-filter:start
// anchor:custom-before-filter:end
try {
if (HttpMethod.OPTIONS.equals(requestContext.getMethod())) {
return;
}
if (!isExcluded(requestContext)) {
SecurityRight securityRight = getSecurityRight(path);
if (securityRight == null) {
// @anchor:default-security:start
// @anchor:default-security:end
// anchor:custom-default-security:start
// anchor:custom-default-security:end
} else if (securityRight.disabled()) {
// @anchor:disabled-security:start
// @anchor:disabled-security:end
// anchor:custom-disabled-security:start
// anchor:custom-disabled-security:end
return;
}
// @anchor:filter:start
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end
if (userContext.isAnonymous()) {
throw new NotAuthorizedException("User is not authorized to access this resource.");
}
}
} finally {
JaxNsContext.updateContext(requestContext, userContext);
}
}
Exclusions
It is possible to define exclusions where specific requested endpoints do not require authorization. The provider has a method for this, where depending on the path this can be handled.
This exclusion mechanism should be used with care and is intended primarily for paths which are not part of the API itself or custom exclusion mechanisms. For endpoints which are to be excluded, it is preferred to use the SecurityRight
annotation instead.
private boolean isExcluded(final ContainerRequestContext containerRequestContext) {
final String path = containerRequestContext.getUriInfo().getPath();
// @anchor:exclude:start
// @anchor:exclude:end
// anchor:custom-exclude:start
// anchor:custom-exclude:end
return false;
}
SecurityRight
Authorization comes with a predefined stub mechanism which is intended to handle security though the definition of rights on the endpoints. The mechanism is stubbed in such a way that the implementation of these rights can be customized.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityRight {
boolean disabled() default false;
// @anchor:methods:start
// @anchor:methods:end
// anchor:custom-methods:start
// anchor:custom-methods:end
}
The @SecurityRight
annotation can be defined on the method of any JAX-RS REST endpoint in the API. The AuthorizationProvider
will automatically retrieve it if present for every call, so it can be processed for authorization.
The idea behind the security rights is that in some form specific rights can be attached to a user. These rights, typically identified by a name can be added to an endpoint through the annotation. If they are identified by a name, a name
field could be added to the SecurityRight
annotation for example. The AuthorizationProvider
would then determine based on the UserContext
if the user has the right(s) defined on this endpoint to access it.
One mechanism that is available in the stubbed implementation is the disabled
field on the SecurityRight
annotation, which is set to true
, will disable authorization for that specific endpoint.
An expanded implementation that leverages SecurityRight
is available for the DataAccessRight
element which is part of the account
base-component.
DataAccessRight integration
Like other security features, it is possible to enable some default integration with the account component for authorization.
Component
Available with the
rest-account-security-expanders
bundle.
This option provides an authorization implementation which integrates with the authorization task in the account
component.
Through this system, rules for access control can be defined in the account::DataAccess
element.
<options>
<jaxrs.account.accessControl.enable/>
</options>
Additional code is injected in the AuthorizationProvider.filter()
method, which processes the authorization through the account component, based on the @DataAccessRight
annotations. If any of the rights configured in the annotations are granted to the user in the account component, the call will be authorized.
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
Context context = JaxNsContext.getContext(requestContext);
MyComponentUserContext userContext = MyComponentUserContext.from(context);
String path = requestContext.getUriInfo().getPath();
// @anchor:before-filter:start
// @anchor:before-filter:end
// anchor:custom-before-filter:start
// anchor:custom-before-filter:end
try {
if (HttpMethod.OPTIONS.equals(requestContext.getMethod())) {
return;
}
if (!isExcluded(requestContext)) {
SecurityRight securityRight = getSecurityRight(path);
if (securityRight == null) {
// @anchor:default-security:start
// @anchor:default-security:end
// anchor:custom-default-security:start
// anchor:custom-default-security:end
} else if (securityRight.disabled()) {
// @anchor:disabled-security:start
// @anchor:disabled-security:end
// anchor:custom-disabled-security:start
// anchor:custom-disabled-security:end
return;
}
// @anchor:filter:start
if (securityRight != null) {
AuthorizationManagerAgent authorizationAgent = AuthorizationManagerAgent.getAuthorizationManagerAgent(context);
for (DataAccessRight dataAccessRight : securityRight.dataAccessRights()) {
DataAccessQuery dataAccessQuery = new AccessQueryBuilder()
.setComponent(dataAccessRight.component())
.setElement(dataAccessRight.dataElement())
.setFunctionality(dataAccessRight.functionality())
.createAccessQuery();
TaskResult<DataAccessRights> dataAccessRights = authorizationAgent.getDataAccessRights(dataAccessQuery);
if (dataAccessRights.isError()) {
throw new NotAuthorizedException("Could not retrieve data access rights for '" + dataAccessQuery + "'.");
}
if (dataAccessRights.getValue().isAllowed(dataAccessRight.functionality())) {
return;
}
}
throw new NotAuthorizedException("User is not authorized to access this resource.");
}
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end
if (userContext.isAnonymous()) {
throw new NotAuthorizedException("User is not authorized to access this resource.");
}
}
} finally {
JaxNsContext.updateContext(requestContext, userContext);
}
}
DataAccessRight annotation
An array field dataAccessRights
is added to the @SecurityRight
annotation and provisions it with a set of @DataAccessRight
annotations which define new API-specific access rights that can be used in the DataAccess component to grant access to the endpoint.
Endpoint | Rights |
---|---|
GET (list) | apiAll , apiGetList , (status ) |
GET (resource) | apiAll , apiGetResource , (status ) |
POST | apiAll , apiCreate , (create ) |
PUT | apiAll , apiModify , (modify ) |
PATCH | apiAll , apiUpdate , (modify ) |
REMOVE | apiAll , apiRemove , (delete ) |
Component
Available with the
rest-account-security-expanders
bundle.
Adds the default access rights (status
, create
, modify
and delete
) that are used in the standard struts-based API, which could make configuration easier if the access control should match between these interfaces.
The option will only have an effect when jaxrs.account.accessControl.enable
is also present.
<options>
<jaxrs.account.accessControl.defaultRights/>
</options>
The @SecurityRight
annotation on a GET
list endpoint with the default rights option included:
@SecurityRight(dataAccessRights = {
@DataAccessRight(component = "testComp", dataElement = "city", functionality = "apiAll"),
@DataAccessRight(component = "testComp", dataElement = "city", functionality = "apiGetList"),
@DataAccessRight(component = "testComp", dataElement = "city", functionality = "status"),
})