Authentication and Authorization
Policies
Authorization decisions often depend not only on the subject and the requested action, but also on
the target object. Thus, Subato uses policy-based access control (attribute-based access control) to
determine whether a subject is allowed to perform an action or access a resource. See
CourseUpdatePolicy for an example policy. Policies are evaluated by PolicyEvaluator.
CourseController#update and CourseService#update demonstrate how policies are enforced during
request processing.
Filtering and pagination requires policies to be translated into SQL. Thus, each policy can produce
a Specification that applies the authorization rules directly to a database query. Additional
domain-specific filters are then added to this Specification object, allowing repository
implementations to return only resources the current user is permitted to access. See the docs on
persistence for more details and SolutionController#getAll for an example.
Data Masking
Some information returned by the backend must be hidden from users who are not authorized to view it. For example, email addresses of lecturers should be masked for anonymous users. Masking is applied automatically before a response is serialized. Each DTO class can provide a corresponding Masker implementation, which removes or transforms values of sensitive fields. See UserMasker for an example.
Each masker must be registered in MaskingModule as follows:
modifier.addMasker(new UserMasker(), UserDto.class);
See the JavaDoc of MaskerSerializerModifier for implementation details.
Streaming Protected Resources
Clients often need to access protected resources, for example to:
- download course, task, or solution files
- display images and other media embedded in course, exercise, or task descriptions
Public resources can simply be linked using the download attribute:
<a [href]="getDownloadLink(file)" download> ... </a>
Protected resources are more challenging because authentication headers cannot be simply attached to
browser-initiated requests. This affects not only downloads triggered by links, but also resources
loaded through elements such as img, iframe, and object.
Possible Approaches
Several approaches can be used to solve this problem:
- Click interceptor. When an user clicks on a link, intercept the click, perform the request
manually with an
Authorizationheader, and expose the downloaded resource throughcreateObjectURL. However, this only works for links, not for elements such asimgandiframe. Moreover, the approach is impractical for large files, and the generated links cannot be shared. - Signed URLs. Generate temporary access tokens (which we refer to as stream tokens) and append them to resource URLs as query parameters. Then, anyone that posesses a signed URL can access the resource without providing an access token in the request header. Such signed URLs must expire after a short time to limit their exposure. They must also be converted back to regular URLs when users edit them through inputs exposed in the client, and sharing resource URLs is challenging because opening them in a browser (without the stream token) always fails authorization.
- Cookies. Store the access token in a cookie after login. The browser automatically includes
the cookie with each request, allowing the backend to use it as a fallback to the
Authorizationheader. Although cookies can be hardened through expiration policies and domain restrictions, they remain vulnerable to certain attack scenarios. Moreover, the access token must be refreshed regularly, so authentication eventually expires if the web app is not kept open.
Implementation in Subato
Up to v2.0.0-beta15, Subato relied exclusively on the first approach. In practice, this proved difficult to maintain and suffered from the limitations described above. Since v2.0.1, Subato combines signed URLs with server-managed sessions as follows:
Embedded resources use signed URLs backed by stream tokens. When HTML content is requested (e.g. a task description), all embedded resource URLs are signed on the server before the response is sent. The browser can then load these resources directly without having to intercept the requests to attach an access token. For cases outside HTML rewriting, clients can explicitly request a signed URL by sending a POST request to the /sign endpoint. This allows protected resources to be embedded in other contexts, for example when displaying a PDF in a solution using the object element.
To make links shareable and direct file downloads work, the backend also provides session-based authentication. When a protected resource is opened directly in the browser, the request is routed through an OAuth2 client. If the user is not yet authenticated, they are redirected to the Keycloak login page. Users who have already authenticated through the web app are redirected immediately back to the requested resource, and the download begins without additional interaction. The access token is stored on the server as part of the user's session rather than in the browser cookie itself. Thus, subsequent requests can be authorized using the existing session, allowing ordinary resource URLs to work without requiring stream tokens. Although this mechanism still relies on cookies, the access token itself never leaves the server, reducing its exposure compared to storing them on the client.
The following components implement the streaming infrastructure:
StreamTokenManagermanages stream tokens and is responsible for creating, signing, and activating them.StreamFilterintercepts incoming requests and delegates token activation toStreamTokenManager.- Controllers sign individual URLs directly through
StreamTokenManager, whileHtmlSignersigns multiple URLs contained in HTML documents. SignedUrlSanitizerautomatically removes stream tokens from URLs entered into input fields by users, ensuring that signed URLs are automatically converted back to ordinary links before storing them.
Refer to the JavaDocs for additional details.