Protect your backend
Securing your backend is even more important than securing your frontend since the backend usually holds sensitive and important information that we never want to return to a user that is either not logged in or not the rightful recipient of that information.
In this guide we'll be showing you how easy it is to secure your application backend with Nblocks. You will apply protection by using the tokens obtained from the logged in user. Below we will Protect an endpoint with role based access control, so that only users with the right privilege can query it.
After you're done with this guide you will have an application backend that is secure and ready for production.
The Nblocks team provides, maintains, and adds code examples for popular languages continuously. However, if you have a specific need not covered by this quickstart yet, you'll find documentation on how to use Nblocks with any language in our API reference API reference.
- You have completed the Quickstart and have access to the tokens obtained from the logged in user.
Short info about the access token
When you completed the Quickstart you got hold of a couple of tokens that was signed securely by Nblocks. One of them, the access token holds the users access information and this information can be used to protect your app. The token is encoded in a format called JWT for greater portability and security. If we decode the access token it looks like this:
{
"aid": "63d2ab029e23db0afb07a5a7",
"tid": "63d2b5c18892e10022e08399",
"scope": "AUTHENTICATED USER_READ USER_WRITE TENANT_READ TENANT_WRITE",
"role": "OWNER",
"plan": "FREE",
"iat": 1685648418,
"exp": 1685652018,
"aud": [
"63d2ab029e23db0afb07a5a7",
"https://app.nblocks.cloud"
],
"iss": "https://auth.nblocks.cloud",
"sub": "63d2b5c18892e10022e083a2"
}
JWTs is a well known concept in security. That means there are plenty of libraries for many software stacks to decode and verify them into readable JSON. Here's a extensive list of different libraries. We'll be using one of these in our code examples.
In the structure above we highlighted the parts that we'll be focusing on in this guide.
- The
role
property tells the role of the user. - The
scope
property tells what privileges the user obtained when logging in as a that role. This can be used for more fine grained protection and role based access control on individual features. - The
sub
property shows the id of the user. - The
tid
property shows the id of the tenant
How will the user provide the token to the backend on each request?
- If your developing an application with both a backend and a frontend it is best practise to let the frontend attach the access token as an
authorization
header on each request to the backend. - When developing a backend only application you could either still resort to clients providing the
authorization
header in each request or store the accessToken in a session cookie once you obtained it in the quickstart. Clients will provide this cookie automatically on every future request.
Step 1. Protect an endpoint with role based access control
- ExpressJS
- Java
- Can't find your stack?
Imagine you have declared two endpoints, /dashboardData
and /analyticsData
in your express app like this.
const app = express();
app.get('/dashboardData', (req, res) => {
...
});
app.get('/analyticsData', (req, res) => {
...
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
We want both of the endpoints to require the user to be logged in before rendering a result.
The /analyticsData
endpoint should only be accessable for users with the privilege ANALYTICS_READ
.
All this can easily be done with Express middlewares:
Create a new file in which we declare two middlewares.
import { jwtVerify, createRemoteJWKSet } from 'jose';
// Verifies and decodes the access token from the authorization header
const jwtMiddleware = async (req, res, next) => {
if (req.headers['authorization']) {
try {
// Get the public key to verify the token signed by Nblocks
const JWKS = createRemoteJWKSet(
new URL('https://auth.nblocks.cloud/.well-known/jwks.json')
);
// Deconstruct user from the verified token and store the decoded information in a variable that can be used by other middlewares
const { payload: user } = await jwtVerify(
req.headers['authorization'], JWKS, { issuer: 'https://auth.nblocks.cloud' }
);
req.user = user;
next();
} catch (e) {
next(e);
}
}
};
// Grants or denies access based on the decoded token from jwtMiddleware
const protectedRoute = ({ roles, privileges }) => {
return (req, res, next) => {
if (req.user) {
if (
roles
? roles.includes(req.user.role)
: false || privileges
? privileges.some((scope) => req.user.scope.includes(scope))
: false
) {
next();
} else {
res.status(403).send({ message: 'Forbidden' });
}
} else {
res.status(401).send({ message: 'Unauthorized' });
}
};
};
export { jwtMiddleware, protectedRoute }
If you're building a backend only app, you might want to redirect users to /auth/login
instead of rendering a 401 Unauthorized
error in case they're unauthorized for a better user experience.
Heading back to the initial express app file we can now make use of these middleware to start protecting the endpoints!
import { jwtMiddleware, protectedRoute } from './middlewares'
const app = express();
// First middleware that decodes tokens
app.use(jwtMiddleware);
// Restrict all endpoints globally with protectedRoute to require authenticated users
app.use(protectedRoute({privileges: ['AUTHENTICATED']}));
app.get('/dashboardData', (req, res) => {
...
});
// Restrict /analyticsData with protectedRoute to require users with ANALYTICS_READ privilege
app.get('/analyticsData', protectedRoute({privileges: ['ANALYTICS_READ']}), (req, res) => {
...
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
More examples
Our protectedRoute
middleware can be activated in other ways, like this:
// Protect all endpoints
app.use(protectedRoute({privileges: ['AUTHENTICATED']}));
// Protect /analyticsData
app.use("/analyticsData", protectedRoute({privileges: ['AUTHENTICATED']}));
// Protect using wildcards
app.use("/secret/*", protectedRoute({roles: ['ADMIN']}));
Imagine you have declared two endpoints, /protected/dashboardData
and /protected/analyticsData
in your Java app like this.
package com.mycompany.app;
import java.util.EnumSet;
import java.util.List;
import javax.servlet.DispatcherType;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
public class App {
public static void main(String[] args) throws Exception {
Server server = new Server(8080);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
server.setHandler(context);
context.addServlet(new ServletHolder(new LoginServlet()), "/login");
context.addServlet(new ServletHolder(new CallbackServlet()), "/auth/oauth-callback");
context.addServlet(new ServletHolder(new DashboardDataServlet()), "/protected/dashboardData");
context.addServlet(new ServletHolder(new AnalyticsDataServlet()), "/protected/analyticsData");
server.start();
server.join();
}
}
We want both of the endpoints to require the user to be logged in before rendering a result.
The /analyticsData
endpoint should only be accessable for users with the privilege ANALYTICS_READ
.
All this can easily be done with two Java servlet filters:
Create a new Filter class, JwtFilter
, in which we declare the verification of JWTs:
package com.mycompany.app;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jwa.AlgorithmConstraints.ConstraintType;
import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JwtFilter implements Filter {
// Replace this with your own APP ID
private static final String APP_ID = "XXX";
public static JwtClaims verifyJwt(String token) throws InvalidJwtException {
String JWKS_URL = "https://auth.nblocks.cloud/.well-known/jwks.json";
try {
// Verify the tokens result using public keys from Nblocks JWKS
HttpsJwks httpsJkws = new HttpsJwks(JWKS_URL);
HttpsJwksVerificationKeyResolver httpsJwksKeyResolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setVerificationKeyResolver(httpsJwksKeyResolver)
.setExpectedIssuer("https://auth.nblocks.cloud")
.setExpectedAudience(APP_ID)
.setJwsAlgorithmConstraints(new AlgorithmConstraints(ConstraintType.PERMIT,
AlgorithmIdentifiers.RSA_PSS_USING_SHA256))
.build();
JwtClaims jwtClaims = jwtConsumer.processToClaims(token);
return jwtClaims;
} catch (InvalidJwtException e) {
e.printStackTrace();
return null;
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String authHeader = httpRequest.getHeader("Authorization");
// Only run verficiation if Authorization header is present
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring("Bearer ".length());
try {
JwtClaims jwtClaims = verifyJwt(token);
httpRequest.setAttribute("user", jwtClaims.getClaimsMap());
} catch (Exception e) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return;
}
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialization logic here
}
@Override
public void destroy() {
// Cleanup logic here
}
}
And create another Filter class, AccessControlFilter
, in which we check the scopes of the requesting user:
package com.mycompany.app;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AccessControlFilter implements Filter {
private List<String> roles;
private List<String> privileges;
public AccessControlFilter(List<String> roles, List<String> privileges) {
this.roles = roles;
this.privileges = privileges;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
Map<String, Object> user = (Map<String, Object>) httpRequest.getAttribute("user");
if (user != null) {
String role = (String) user.get("role");
String scope = (String) user.get("scope");
List<String> scopeList = List.of(scope.split(" "));
boolean roleCheck = roles != null ? roles.contains(role) : false;
boolean privilegeCheck = privileges != null ? scopeList.stream().anyMatch(privileges::contains) : false;
if (roleCheck || privilegeCheck) {
chain.doFilter(request, response);
} else {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
}
} else {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialization logic here
}
@Override
public void destroy() {
// Cleanup logic here
}
}
If you're building a backend only app, you might want to redirect users to /auth/login
instead of rendering a 401 Unauthorized
error in case they're unauthorized for a better user experience.
Heading back to the initial Java app file, we can now make use of these filters to start protecting the endpoints!
package com.mycompany.app;
import java.util.EnumSet;
import java.util.List;
import javax.servlet.DispatcherType;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
public class App {
public static void main(String[] args) throws Exception {
Server server = new Server(8080);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
server.setHandler(context);
context.addServlet(new ServletHolder(new LoginServlet()), "/login");
context.addServlet(new ServletHolder(new CallbackServlet()), "/auth/oauth-callback");
context.addServlet(new ServletHolder(new DashboardDataServlet()), "/protected/dashboardData");
context.addServlet(new ServletHolder(new AnalyticsDataServlet()), "/protected/analyticsData");
// Process all incoming requests with JwtFilter process and verify the JWT
context.addFilter(new FilterHolder(new JwtFilter()), "/*", EnumSet.of(DispatcherType.REQUEST));
// Restrict all protected endpoints globally with AccessControlFilter to require authenticated users
context.addFilter(new FilterHolder(new AccessControlFilter(null, List.of("AUTHENTICATED"))),
"/protected/*", EnumSet.of(DispatcherType.REQUEST));
// Restrict /analyticsData with AccessControlFilter to require users with ANALYTICS_READ privilege
context.addFilter(new FilterHolder(new AccessControlFilter(null, List.of("ANALYTICS_READ"))),
"/protected/analyticsData/*", EnumSet.of(DispatcherType.REQUEST));
server.start();
server.join();
}
}
More examples
Our AccessControlFilter
filter can be activated in other ways, like this:
// Protect all endpoints with AUTHENTICATED privilege
context.addFilter(new FilterHolder(new AccessControlFilter(null, List.of("AUTHENTICATED"))),
"/*", EnumSet.of(DispatcherType.REQUEST));
// Protect /protected/analyticsData with AUTHENTICATED privilege
context.addFilter(new FilterHolder(new AccessControlFilter(null, List.of("AUTHENTICATED"))),
"/protected/analyticsData/", EnumSet.of(DispatcherType.REQUEST));
// Protect using wildcards with ADMIN role
context.addFilter(new FilterHolder(new AccessControlFilter(List.of("ADMIN"), null)),
"/secret/*", EnumSet.of(DispatcherType.REQUEST));
If you cannot find your stack in the example code you need to integrate this part manually.
Nblocks can be integrated with any stack or framework.
Essentially it's all about REST API calls or redirecting the user browser agent which is documented in more details in the API reference https://nebulr-group.github.io/nblocks-api-docs.
Use the other code examples to understand what should to be done and in what order.
We'd be happy to get feedback so we can add more code examples so don't forget to make a request, by joining our Discord https://discord.gg/kjWYdZ6f6G and spell it out.
Next steps
- Protect your frontend with user tokens.
- Add Nblocks hosted user management to your app.
- Add SSO alternatives to your login experience.
If you haven't already, join our Discord