We were testing a REST webservice developed in jersey through postman rest client. It is a POST method and is annotated with #RolesAllowed. The full annotation the method is as follows:
#POST
#Path("/configuration")
#RolesAllowed("admin")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
When I requested this http://baseurl/configuration with the expected HTTP body content, I got 403 response(it is expected since it is allowed only for admin as it seems).
My doubt is how to access this service with the specified role via rest client.
So it seems like you set up the RolesAllowedDynamicFeature, but you have no authentication happening to set up the user and roles. What the RolesAllowedDynamicFeature does is lookup the SecurityContext, and calls the SecurityContext.isUserInRole(<"admin">) to see if the user in the SecurityContext has the role.
I imagine you don't know how the SecurityContext is set. There are a couple of ways. The first is through the servlet authentication mechanism. You can see more at Securing Web Applications from the Java EE tutorial.
Basically you need to set up a security realm or security domain on the server. Every server has it's own specific way of setting it up. You can see an example here or how it would be done with Tomcat.
Basically the realm/domain contains the users allowed to access the web app. Those users have associated roles. When the servlet container does the authentication, whether it be Basic authentication or Form authentication, it looks up the user from the credentials, and if the user is authenticated, the user and its roles are associated with the request. Jersey gathers this information and puts it into the SecurityContext for the request.
If this seems a bit complicated, an easier way to just forget the servlet container authentication and just create a Jersey filter, where you set the SecurityContext yourself. You can see an example here. You can use whatever authentication scheme you want. The important part is setting the SecurityContext with the user information, wherever you get it from, maybe a service that accesses a data store.
See Also:
securing rest services in Jersey
UPDATE
Here is a complete example of the second option using the filter. The test is run by Jersey Test Framework. You can run the test as is
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Priority;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.DatatypeConverter;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.internal.util.Base64;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.test.JerseyTest;
import static junit.framework.Assert.*;
import org.junit.Test;
public class BasicAuthenticationTest extends JerseyTest {
#Provider
#Priority(Priorities.AUTHENTICATION)
public static class BasicAuthFilter implements ContainerRequestFilter {
private static final Logger LOGGER = Logger.getLogger(BasicAuthFilter.class.getName());
#Inject
private UserStore userStore;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String authentication = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authentication == null) {
throw new AuthenticationException("Authentication credentials are required");
}
if (!authentication.startsWith("Basic ")) {
return;
}
authentication = authentication.substring("Basic ".length());
String[] values = new String(DatatypeConverter.parseBase64Binary(authentication),
Charset.forName("ASCII")).split(":");
if (values.length < 2) {
throw new WebApplicationException(400);
}
String username = values[0];
String password = values[1];
LOGGER.log(Level.INFO, "{0} - {1}", new Object[]{username, password});
User user = userStore.getUser(username);
if (user == null) {
throw new AuthenticationException("Authentication credentials are required");
}
if (!user.password.equals(password)) {
throw new AuthenticationException("Authentication credentials are required");
}
requestContext.setSecurityContext(new MySecurityContext(user));
}
}
static class MySecurityContext implements SecurityContext {
private final User user;
public MySecurityContext(User user) {
this.user = user;
}
#Override
public Principal getUserPrincipal() {
return new Principal() {
#Override
public String getName() {
return user.username;
}
};
}
#Override
public boolean isUserInRole(String role) {
return role.equals(user.role);
}
#Override
public boolean isSecure() { return true; }
#Override
public String getAuthenticationScheme() {
return "Basic";
}
}
static class AuthenticationException extends WebApplicationException {
public AuthenticationException(String message) {
super(Response
.status(Status.UNAUTHORIZED)
.header("WWW-Authenticate", "Basic realm=\"" + "Dummy Realm" + "\"")
.type("text/plain")
.entity(message)
.build());
}
}
class User {
public final String username;
public final String role;
public final String password;
public User(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
}
class UserStore {
public final Map<String, User> users = new ConcurrentHashMap<>();
public UserStore() {
users.put("peeskillet", new User("peeskillet", "secret", "USER"));
users.put("stackoverflow", new User("stackoverflow", "superSecret", "ADMIN"));
}
public User getUser(String username) {
return users.get(username);
}
}
private static final String USER_RESPONSE = "Secured User Stuff";
private static final String ADMIN_RESPONSE = "Secured Admin Stuff";
private static final String USER_ADMIN_STUFF = "Secured User Admin Stuff";
#Path("secured")
public static class SecuredResource {
#GET
#Path("userSecured")
#RolesAllowed("USER")
public String getUser() {
return USER_RESPONSE;
}
#GET
#Path("adminSecured")
#RolesAllowed("ADMIN")
public String getAdmin() {
return ADMIN_RESPONSE;
}
#GET
#Path("userAdminSecured")
#RolesAllowed({"USER", "ADMIN"})
public String getUserAdmin() {
return USER_ADMIN_STUFF;
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(SecuredResource.class)
.register(BasicAuthFilter.class)
.register(RolesAllowedDynamicFeature.class)
.register(new AbstractBinder(){
#Override
protected void configure() {
bind(new UserStore()).to(UserStore.class);
}
});
}
static String getBasicAuthHeader(String username, String password) {
return "Basic " + Base64.encodeAsString(username + ":" + password);
}
#Test
public void should_return_403_with_unauthorized_user() {
Response response = target("secured/userSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("stackoverflow", "superSecret"))
.get();
assertEquals(403, response.getStatus());
}
#Test
public void should_return_200_response_with_authorized_user() {
Response response = target("secured/userSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("peeskillet", "secret"))
.get();
assertEquals(200, response.getStatus());
assertEquals(USER_RESPONSE, response.readEntity(String.class));
}
#Test
public void should_return_403_with_unauthorized_admin() {
Response response = target("secured/adminSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("peeskillet", "secret"))
.get();
assertEquals(403, response.getStatus());
}
#Test
public void should_return_200_response_with_authorized_admin() {
Response response = target("secured/adminSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("stackoverflow", "superSecret"))
.get();
assertEquals(200, response.getStatus());
assertEquals(ADMIN_RESPONSE, response.readEntity(String.class));
}
}
Here is the only dependency needed to run the test
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey2.version}</version>
<scope>test</scope>
</dependency>
Related
I am trying to implement mfa authentication in my app using totp. Bellow is the library i use. All is going well for registering the user, i receive the qr code, scan it and get every 30 secs the code in google authenticator. When i am trying to login to verify the code, the code verification doesnt work (in auth service, method Verify). I've spent several hours but cant figure it out, tried different users, logs but without success.
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>
this is my code
AuthContoller.java
import com.example.jsonfaker.model.dto.LoginRequest;
import com.example.jsonfaker.model.dto.SignupRequest;
import com.example.jsonfaker.model.dto.VerifyRequest;
import com.example.jsonfaker.service.Exporter;
import com.example.jsonfaker.service.UserAuthService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
#RestController
#RequestMapping("/auth")
#CrossOrigin
public class AuthController {
private final Exporter exporter;
private final UserAuthService userAuthService;
public AuthController(Exporter exporter, UserAuthService userAuthService) {
this.exporter = exporter;
this.userAuthService = userAuthService;
}
#PostMapping("/login")
public ResponseEntity<String> authenticateUser(#Valid #RequestBody LoginRequest loginRequest) {
String response = userAuthService.login(loginRequest);
return ResponseEntity
.ok()
.body(response);
}
#PostMapping("/register2FA")
public ResponseEntity<byte[]> registerUser2FA(#Valid #RequestBody SignupRequest signupRequest) throws Exception {
userAuthService.register2FA(signupRequest);
byte[] qrCodeBytes = userAuthService.mfaAccountSetup(signupRequest.getUsername());
return ResponseEntity
.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=\""+exporter.exportFileNameQR() + ".png\"")
.body(qrCodeBytes);
}
#PostMapping("/register")
public ResponseEntity<?> registerUser(#Valid #RequestBody SignupRequest signupRequest) throws Exception {
userAuthService.simpleRegister(signupRequest);
return ResponseEntity.ok(HttpStatus.CREATED);
}
#PostMapping("/verify")
public ResponseEntity<String> authenticateUser2FA(#Valid #RequestBody VerifyRequest verifyRequest) throws Exception {
String response = userAuthService.verify(verifyRequest.getUsername(), verifyRequest.getCode());
return ResponseEntity
.ok()
.body(response);
}
}
this is my token manager
import dev.samstevens.totp.code.*;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import dev.samstevens.totp.time.TimeProvider;
import dev.samstevens.totp.util.Utils;
import org.springframework.stereotype.Service;
#Service("mfaTokenManager")
public class DefaultMFATokenManager implements MFATokenManager {
private final SecretGenerator secretGenerator;
private final QrGenerator qrGenerator;
private final CodeVerifier codeVerifier;
public DefaultMFATokenManager(SecretGenerator secretGenerator, QrGenerator qrGenerator, CodeVerifier codeVerifier) {
this.secretGenerator = secretGenerator;
this.qrGenerator = qrGenerator;
this.codeVerifier = codeVerifier;
}
#Override
public String generateSecretKey() {
return secretGenerator.generate();
}
#Override
public String getQRCode(String secret) throws QrGenerationException {
QrData data = new QrData.Builder().label("MFA")
.secret(secret)
.issuer("Daniel token")
.algorithm(HashingAlgorithm.SHA1)
.digits(6)
.period(30)
.build();
return Utils.getDataUriForImage(
qrGenerator.generate(data),
qrGenerator.getImageMimeType()
);
}
#Override
public boolean verifyTotp(String code, String secret) {
TimeProvider timeProvider = new SystemTimeProvider();
CodeGenerator codeGenerator = new DefaultCodeGenerator();
CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
System.out.println(timeProvider.getTime());
System.out.println(codeGenerator);
return verifier.isValidCode(secret, code);
}
}
this is my auth service
import com.example.jsonfaker.model.Roles;
import com.example.jsonfaker.model.SystemUser;
import com.example.jsonfaker.model.dto.LoginRequest;
import com.example.jsonfaker.model.dto.SignupRequest;
import com.example.jsonfaker.model.dto.TokenResponse;
import com.example.jsonfaker.repository.RolesRepository;
import com.example.jsonfaker.repository.SystemUserRepository;
import com.example.jsonfaker.security.AuthoritiesConstants;
import com.example.jsonfaker.security.jwt.JwtUtils;
import com.example.jsonfaker.twoFA.MFATokenManager;
import com.example.jsonfaker.twoFA.MfaTokenData;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.stream.Collectors;
import static java.util.Objects.nonNull;
#Service
public class UserAuthService {
private final SystemUserRepository systemUserRepository;
private final RolesRepository rolesRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final MFATokenManager mfaTokenManager;
private final AuthenticationManager authenticationManager;
private final LoginUserService loginUserService;
private final JwtUtils jwtUtils;
public UserAuthService(SystemUserRepository systemUserRepository, RolesRepository rolesRepository, BCryptPasswordEncoder bCryptPasswordEncoder, MFATokenManager mfaTokenManager, AuthenticationManager authenticationManager, LoginUserService loginUserService, JwtUtils jwtUtils) {
this.systemUserRepository = systemUserRepository;
this.rolesRepository = rolesRepository;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.mfaTokenManager = mfaTokenManager;
this.authenticationManager = authenticationManager;
this.loginUserService = loginUserService;
this.jwtUtils = jwtUtils;
}
public void simpleRegister(SignupRequest signupRequest) throws Exception {
if(systemUserRepository.findByUsername(signupRequest.getUsername()).isPresent()){
throw new Exception("User with this username exists");
}
Roles simpleUserRole = new Roles();
simpleUserRole.setName(AuthoritiesConstants.USER);
SystemUser user = new SystemUser();
user.setPassword(bCryptPasswordEncoder.encode(signupRequest.getPassword()));
user.setUsername(signupRequest.getUsername());
user.setAuthorities(rolesRepository.findAllByName("ROLE_USER").stream().collect(Collectors.toSet()));
user.setSecret(mfaTokenManager.generateSecretKey());
systemUserRepository.save(user);
}
public void register2FA(SignupRequest signupRequest) throws Exception {
if(systemUserRepository.findByUsername(signupRequest.getUsername()).isPresent()){
throw new Exception("User with this username exists");
}
Roles simpleUserRole = new Roles();
simpleUserRole.setName(AuthoritiesConstants.USER);
SystemUser user = new SystemUser();
user.setPassword(bCryptPasswordEncoder.encode(signupRequest.getPassword()));
user.setUsername(signupRequest.getUsername());
user.setAuthorities(rolesRepository.findAllByName("ROLE_USER").stream().collect(Collectors.toSet()));
user.setTwoFAisEnabled(Boolean.TRUE);
user.setSecret(mfaTokenManager.generateSecretKey());
systemUserRepository.save(user);
}
public byte[] mfaAccountSetup(String username) throws Exception {
SystemUser user = systemUserRepository.findByUsername(username).get();
if (!nonNull(user)){
throw new Exception("Unable to find user with this username");
}
if(!user.isTwoFAisEnabled()){
throw new Exception("2FA is not enabled for this account");
}
MfaTokenData token = new MfaTokenData(mfaTokenManager.getQRCode(user.getSecret()), user.getSecret());
System.out.println("Mfa code :" +token.getMfaCode());
String base64Image = token.getQrCode().split(",")[1];
byte[] imageBytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(base64Image);
return imageBytes;
}
public String login(LoginRequest loginRequest){
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
if(systemUserRepository.findByUsername(loginRequest.getUsername()).get().isTwoFAisEnabled()){
return "verify code now";
}
SystemUser userDetails = (SystemUser) authentication.getPrincipal();
String jwt = jwtUtils.generateJwtToken(userDetails);
return new TokenResponse(jwt).toString();
}
public String verify(String username, String code) throws Exception {
SystemUser user = systemUserRepository.findByUsername(username).get();
if (!nonNull(user)){
throw new Exception("Unable to find user with this username");
}
if (!mfaTokenManager.verifyTotp(code, user.getSecret())){
return "unable to auth";
}
return "token here";
}
}
Finally found the problem, on my phone the time was delayed by 2 minutes, I've set it to be the same as on my computer and worked. The problem is that when validating the token the app uses an interval of 30 seconds for each token generation, and if the delay on the phone or other device is bigger than 30 sec in future or past the timestamp doesnt match the one used for verification.
Here is the documentation for the library i used, make sure to read it carefully before using it.
https://github.com/samdjstevens/java-totp
Here is the article which i followed in my project:
https://www.javadevjournal.com/spring-security/two-factor-authentication-with-spring-security/
Useful reading before starting a project using TOTP:
https://www.freecodecamp.org/news/how-time-based-one-time-passwords-work-and-why-you-should-use-them-in-your-app-fdd2b9ed43c3/
A youtube video about 2FA:
https://www.youtube.com/watch?v=ZXFYT-BG2So
I'm trying to work out a simple web app with angular as the front end and Spring Boot as the back end. My pre-flight request succeeds and I receive a valid token, however when trying to make a subsequent request to a route I get a 404. When I try to curl the url I get a 500 with the message "Missing or invalid authorization header" which seems to me to be saying that the route does indeed exist and is listening, but something else is wrong.
First of all the Typescript. Here is my login.component.ts
import { Component } from "#angular/core";
import { Observable } from "rxjs/index";
import { LoginService } from "../services/login.service";
#Component({
selector: "login",
templateUrl: "./login.component.html"
})
export class Login {
private model = {"username": "", "password": ""};
private currentUserName;
constructor (private loginService: LoginService) {
this.currentUserName = localStorage.getItem("currentUserName");
}
public onSubmit() {
console.log("submitting");
this.loginService.sendCredential(this.model).subscribe(
data => {
console.log(data);
localStorage.setItem("token", data);
console.log("Setting token");
this.loginService.sendToken(localStorage.getItem("token")).subscribe(
data => {
this.currentUserName = this.model.username;
localStorage.setItem("currentUserName", this.model.username);
this.model.username = "";
this.model.password = "";
},
error => console.log(error)
);
},
error => {
console.log("oh no");
console.log(error)
}
);
}
}
And then my LoginService
import { Injectable } from "#angular/core";
import { HttpClient } from '#angular/common/http';
import { HttpHeaders } from '#angular/common/http';
import { Observable } from "rxjs/index";
#Injectable()
export class LoginService {
token: string;
constructor (private http: HttpClient) {}
public sendCredential(model): Observable<String> {
const tokenUrlPreFlight = "http://localhost:8080/users/login/";
const httpOptions: {} = {
headers: new HttpHeaders({
'ContentType': 'application/json'
})
};
return this.http.post<String>(tokenUrlPreFlight, model, httpOptions);
}
public sendToken(token) {
const tokenUrlPostFlight = "http://localhost:8080/rest/users/";
console.log("Bearer " + token);
const httpOptions: {} = {
headers: new HttpHeaders({
'Authorization': 'Bearer ' + token
})
};
return this.http.get(tokenUrlPostFlight, httpOptions);
}
public logout(): void {
localStorage.setItem("token", "");
localStorage.setItem("currentUserName", "");
alert("You just logged out");
}
public checkLogin(): boolean {
if(localStorage.getItem("currentUserName") != null && localStorage.getItem("currentUserName") != "" && localStorage.getItem("token") != null && localStorage.getItem("token") != "") {
console.log(localStorage.getItem("currentUserName"));
console.log(localStorage.getItem("token"));
return true;
}
return false;
}
}
And now for the java. First here is my entry point:
import com.acb.app.configuration.JwtFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
#SpringBootApplication
public class App {
#Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean<JwtFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new JwtFilter());
filterRegistrationBean.addUrlPatterns("/rest/*");
return filterRegistrationBean;
}
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
My UserController.java
import com.acb.app.model.User;
import io.jsonwebtoken.*;
import com.acb.maki.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
#RestController
#RequestMapping("/users")
public class UserController {
#Autowired
private UserService userService;
#GetMapping("")
public List<User> userIndex() {
return userService.getAllUsers();
}
#GetMapping("{username}")
public Optional<User> getUserByUsername(#RequestBody String username) {
return userService.findByUsername(username);
}
#PostMapping("login")
public String login(#RequestBody Map<String, String> json) throws ServletException {
if(json.get("username") == null || json.get("password") == null) {
throw new ServletException("Please fill in username and password");
}
final String username = json.get("username");
final String password = json.get("password");
Optional<User> optionalUser = userService.findByUsername(username);
if(!optionalUser.isPresent()) {
throw new ServletException("Username not found");
}
User user = optionalUser.get();
if(!password.equals(user.getPassword())) {
throw new ServletException("Invalid login. Please check username and password");
}
final String response = Jwts.builder().setSubject(username).claim("roles", "user").setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, "secretKey").compact();
final String jsonResponse = String.format("\"%s\"", response);
System.out.println(jsonResponse);
return jsonResponse;
}
#PostMapping(value="/register")
public User register(#RequestBody User user) {
return userService.save(user);
}
}
And last but not least my JwtFilter.java
import io.jsonwebtoken.*;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String authHeader = request.getHeader("Authorization");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
filterChain.doFilter(servletRequest, servletResponse);
} else {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new ServletException("Missing or invalid authorization header");
}
final String token = authHeader.substring(7);
try {
final JwtParser jwtParser = Jwts.parser();
final Jws<Claims> claimsJws = jwtParser.setSigningKey("secretKey").parseClaimsJws(token);
final Claims claims = claimsJws.getBody();
request.setAttribute("claims", claims);
} catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
I am very aware of the bad practices here with comparing plain text passwords, etc. I'm just trying to get this to work for the time being. The token is correctly generated and returned. The token is also set correctly to local storage, but upon making the get request to the /rest/users route a 404 is returned. There are no errors on the java side.
So as I expected, I am indeed an idiot. As the user mentioned above my service maps to /users/ and what I needed is the protected version /rest/users. So next to my UserController I created UserResource.java which looks like so:
import com.acb.app.model.User;
import com.acb.app.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
#RestController
#RequestMapping("/rest")
public class UserResource {
#Autowired
private UserService userService;
#RequestMapping("/users")
public List<User> findAllUsers() {
return userService.getAllUsers();
}
}
And this makes use of the JwtFilter and allows me to protect my routes. Hopefully this will be helpful to someone else.
I have a RequestInterceptor class which is registered in InterceptorRegistry.
For every request to the application, I have used the preHandle() of HandlerInterceptorAdapter Class in RequestInterceptor Class, to Log all the request in Database.
My requirement is to avoid some of the URI coming to RequestInterceptor, in short, they are mandatory for application but I don't want to store that URI in Database.
Can you suggest what are the possible approach I can use to filter out the requests?
I thought using property file but this will create overhead in each request to preHandle().
Configuration File :
#Configuration
public class AppConfig extends WebMvcConfigurerAdapter {
#Autowired
RequestInterceptor requestInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestInterceptor);
}
}
RequestInterceptor Class: I am able to get URI using action variable
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
#Slf4j
#Component
public class RequestInterceptor extends HandlerInterceptorAdapter {
#Autowired
private UserAccessLogsService userAccessLogsService;
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object object) {
try {
String fullUrl;
/*returns the part of the full URL before query string separator character '?' */
StringBuilder requestURL;
requestURL = new StringBuilder(request.getRequestURL().toString());
/*returns the part of the full URL after query string separator character '?' */
String queryString = request.getQueryString();
/*returns URI */
String action = request.getRequestURI();
/*returns Remote Address */
String remoteAddress = request.getRemoteAddr();
if (queryString == null) {
fullUrl = requestURL.toString();
} else {
fullUrl = requestURL.append('?').append(queryString).toString();
}
/*returns loged in user */
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userName = auth.getName();
UserAccessLogs userAccessLogs = new UserAccessLogs(LocalDateTime.now(), userName, remoteAddress, action, fullUrl);
userAccessLogsService.save(userAccessLogs);
} catch (Exception e) {
LOG.error("Error occurred while adding access logs for user ", e);
}
return true;
}
}
For eg: Below are the URI which is coming to application and stored in DB,
/ui/report/user_access_logs
/ui/report/user_activity
/api/systemuser/search
Now out of above URI I want this /api/systemuser/search not to store in DB
it would be best if there is a document or example that is available to the public to show how to integrate Angular http(with credentials) with Spring Security.
I have a way to login which I will show the code below, but I think there must be a better way. Maybe with the option in the Http header withCredentials, but where you provide your credentials?
It is keeping idToken from external auth. service (Google+) and type ( determine type of the auth. service) in headers, so you dont need pass them as a request parameter or as a path variable.
Then in the backend (Spring Java), there is a spring AOP that save the user to the SecurityContext after verification.
Angular Http Call
import { Http, Headers, RequestOptions } from '#angular/http';
...
constructor(private http: Http...){...}
...
search(){
let options ;
if (this.loginService.user) {
let headers = new Headers({ 'idToken': this.loginService.user.idToken,'type':this.loginService.user.type});
options = new RequestOptions({ headers: headers });
}
return this.http
.get("searchurl",options)
...
GooglePlusAuthService
import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
#Service
public class GooglePlusAuthService implements AuthenticationService{
private Logger logger = LoggerFactory.getLogger(GooglePlusAuthService.class);
private static String clientId;
#Value("${client_id.google}")
public void setClientId(String clientId){
GooglePlusAuthService.clientId=clientId;
}
#Override
public void login(String token) {
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())//.setIssuer(clientId)
.setAudience(Collections.singletonList(clientId))
.build();
GoogleIdToken idToken;
try {
idToken = verifier.verify(token);
if (idToken != null) {
Payload payload = idToken.getPayload();
User user = new User();
user.setId(Authenticator.AUTH_TYPE_GOOGLE+"_"+payload.getSubject());
user.setUsername((String) payload.get("name"));
user.setToken(token);
AuthenticationUtils.setUser(user);
} else {
logger.info("Failed to login with Google plus. Invalid ID token.");
}
} catch (Exception e) {
e.printStackTrace();
logger.error("Failed to login with Google plus." + e.getMessage());
}
}
}
AuthenticationUtils
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class AuthenticationUtils {
public static void setUser(User user){
Authentication authentication = new UsernamePasswordAuthenticationToken(user, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
public static User getUser(){
if(SecurityContextHolder.getContext().getAuthentication()!=null)
return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return null;
}
}
This code has a bug which I am also trying to find out.
Why AuthenticationUtils.getUser() is giving me the last signed in user when I didn't provide any credential info. I just opened the url with a private browser and it gets me the last signed in user in the backend.
I want to expose a resource using RESTlet with a fine-grained authentication. My ServerResource should be accessable via GET only for authenticated members (using BASIC Authentication). However, requests using POST should be available also for callers without any authentication.
In order to clearify:
http://path/myapp/user should allow anyone to register using POST, but only registered members should be able to GET a list of all users.
I'm unfortunately not much into RESTlet and I only find examples using coarser authentication for whole Restlets or Routers.
So how do I enable optional authentication for resources and check them on a per-method level?
Thanks in advance!
To do basic authentication in RESTlet 2.0 (I assume you're using 2.0 since you mention ServerResource), you need to use a ChallengeAuthenticator. If this is configured with optional = true then authentication will only be requested if you invoke ChallengeAuthenticator.challenge().
You can create your application with an authenticate() method, and call this whenever you need access to a resource to be secured:
Application:
package example;
import org.restlet.*;
import org.restlet.data.ChallengeScheme;
import org.restlet.routing.Router;
import org.restlet.security.*;
public class ExampleApp extends Application {
private ChallengeAuthenticator authenticatior;
private ChallengeAuthenticator createAuthenticator() {
Context context = getContext();
boolean optional = true;
ChallengeScheme challengeScheme = ChallengeScheme.HTTP_BASIC;
String realm = "Example site";
// MapVerifier isn't very secure; see docs for alternatives
MapVerifier verifier = new MapVerifier();
verifier.getLocalSecrets().put("user", "password".toCharArray());
ChallengeAuthenticator auth = new ChallengeAuthenticator(context, optional, challengeScheme, realm, verifier) {
#Override
protected boolean authenticate(Request request, Response response) {
if (request.getChallengeResponse() == null) {
return false;
} else {
return super.authenticate(request, response);
}
}
};
return auth;
}
#Override
public Restlet createInboundRoot() {
this.authenticatior = createAuthenticator();
Router router = new Router();
router.attach("/user", UserResource.class);
authenticatior.setNext(router);
return authenticatior;
}
public boolean authenticate(Request request, Response response) {
if (!request.getClientInfo().isAuthenticated()) {
authenticatior.challenge(response, false);
return false;
}
return true;
}
}
Resource:
package example;
import org.restlet.data.MediaType;
import org.restlet.representation.EmptyRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.resource.ServerResource;
public class UserResource extends ServerResource {
#Override
public Representation get() {
ExampleApp app = (ExampleApp) getApplication();
if (!app.authenticate(getRequest(), getResponse())) {
// Not authenticated
return new EmptyRepresentation();
}
// Generate list of users
// ...
}
#Override
public Representation post(Representation entity) {
// Handle post
// ...
}
}
I'm presently using Restlet v2.0.10.
The problem with ChallengeAuthenticator.isOptional() is that it's all or nothing. An alternative to the answer provided by #sea36 above is to override ChallengeAuthenticator.beforeHandle() to either perform authentication or skip it based on request method. For example, the resource below will only require authentication when the GET method is used.
Application:
package example;
import org.restlet.*;
import org.restlet.data.ChallengeScheme;
import org.restlet.routing.Router;
import org.restlet.security.ChallengeAuthenticator;
import org.restlet.security.MapVerifier;
public class ExampleApp extends Application {
private ChallengeAuthenticator createAuthenticator() {
Context context = getContext();
ChallengeScheme challengeScheme = ChallengeScheme.HTTP_BASIC;
String realm = "Example site";
// MapVerifier isn't very secure; see docs for alternatives
MapVerifier verifier = new MapVerifier();
verifier.getLocalSecrets().put("user", "password".toCharArray());
ChallengeAuthenticator authOnGet = new ChallengeAuthenticator(context, challengeScheme, realm) {
#Override
protected int beforeHandle(Request request, Response response) {
if (request.getMethod() == Method.GET)
return super.beforeHandle(request, response);
response.setStatus(Status.SUCCESS_OK);
return CONTINUE;
}
};
return authOnGet;
}
#Override
public Restlet createInboundRoot() {
ChallengeAuthenticator userResourceWithAuth = createAuthenticator();
userResourceWithAuth.setNext(UserResource.class);
Router router = new Router();
router.attach("/user", userResourceWithAuth);
return router;
}
}
Resource:
package example;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.representation.Representation;
import org.restlet.resource.ServerResource;
public class UserResource extends ServerResource {
#Get
public Representation listUsers() {
// retrieve list of users and generate response
// ...
}
#Post
public void register(Representation entity) {
// handle post
// ...
}
}
Note that this example applies the policy of authenticating on GET only to the UserResource and not other resources handled by the router.