Wiki

Clone wiki

limeds-framework / Guide To Authentication & Authorization

By default, the framework does not implement authentication or authorization for HTTP endpoints, because we didn't want to constrain developers to a specific security model. However, a hook exists that allows the addition of a custom security provider that automatically ties in with the module of LimeDS that handles the HTTP endpoints.

This is done by implementing the HttpAuthProvider interface and registering it as an OSGi service. Only one method needs to be implemented:

/**
 * Check if the request is authorized.
 * 
 * @param request
 *            The http request to be checked
 * @param response
 *            The http response object, allows altering the response (in
 *            case the request is not valid, e.g. as error reporting)
 * @param httpOperation
 *            A descriptor of the HTTP operation to which the request is
 *            sent
 * 
 * @return An optional object containing authentication information provided
 *         by the system, or <em>empty</em> if the request has not been
 *         authorized.
 *         <p>
 *         The JsonValue containing this authentication information is
 *         automatically added by the framework as the last argument of the
 *         apply-function call.
 */
Optional<JsonValue> authorizeRequest(HttpServletRequest request, HttpServletResponse response,
        HttpOperationDescriptor httpOperation);

For example, a simple password protected implementation could be:

@Segment
public class TokensProvider implements FunctionalSegment, HttpAuthProvider {

    private static final String GLOBAL_PASSWORD = "admin";
    private static final String TOKEN_ATTRIBUTE = "token";

    // Mapping of the valid tokens to the corresponding user id
    private Map<String, String> activeTokens = new HashMap<>();

    @HttpOperation(method = HttpMethod.GET, path = "/tokens")
    @Override
    public JsonValue apply(JsonValue... input) throws Exception {
        JsonValue request = input[1].get("request");
        JsonValue response = input[1].get("response");

        if (GLOBAL_PASSWORD.equals(request.get("query").getString("password")) && request.get("query").has("userid")) {
            // Create access token
            UUID uniqueKey = UUID.randomUUID();
            String strToken = Base64.getUrlEncoder().encodeToString(uniqueKey.toString().getBytes());
            activeTokens.put(strToken, request.get("query").getString("userid"));

            // Write the token in the HTTP session
            response.get("headers").asObject().put("Set-Cookie",
                    TOKEN_ATTRIBUTE + "=" + strToken + ";Path=/; HttpOnly");

            // Return empty body
            return null;
        } else {
            throw new ExceptionWithStatus(401);
        }
    }

    @Override
    public Optional<JsonValue> authorizeRequest(HttpServletRequest request, HttpServletResponse response,
            HttpOperationDescriptor httpOperation) {
        // Parse the token from the HTTP request
        Optional<String> token = parseToken(request);

        // Look up this token in memory
        if (token.isPresent() && activeTokens.containsKey(token.get())) {
            /*
             * If present, return authentication information, indicating
             * success.
             */
            return Optional.of(Json.objectBuilder().add("userid", activeTokens.get(token.get())).build());
        } else {
            // If not, write an error HTTP response...
            response.setHeader("WWW-Authenticate", "session-cookie: " + TOKEN_ATTRIBUTE);
            try {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
                        "Valid credentials are required to access this operation!");
            } catch (IOException e) {
                e.printStackTrace();
            }
            // ... and return an empty optional, indicating failure.
            return Optional.empty();
        }
    }

    private Optional<String> parseToken(HttpServletRequest request) {
        if (request.getHeader("Cookie") != null) {
            return Arrays.stream(request.getHeader("Cookie").split(";")).map(s -> s.trim())
                    .filter(s -> s.startsWith(TOKEN_ATTRIBUTE)).map(s -> s.split("=")[1].trim()).findAny();
        }
        return Optional.empty();
    }

}

As you can see, it implements both the FunctionalSegment and the HttpAuthProvider interface. Being a Segment, we can use LimeDS to expose the function as a HTTP endpoint that gives out tokens to users with a valid userid & password combination. The HttpAuthProvider implementation is picked up by the LimeDS HTTP Manager and the authorizeRequest method is now automatically called each time a HTTP endpoint (implemented as a FunctionalSegment) for which Authentication or Authorization is configured, is called.

Notice that we use Cookies to write the generated token to the Http Session upon successful authentication.

Updated