How to have access to token in header to pass it to thymeleaf to be able to do ajax call - spring-boot

I use spring boot with spring cloud gateway
I have another app with spring boot and thymeleaf
Spring gateway return a token to my thymeleaf app.
#EnableWebFluxSecurity
#Configuration
public class WebFluxSecurityConfig {
#Autowired
private WebFluxAuthManager authManager;
#Bean
protected SecurityWebFilterChain securityFilterChange(ServerHttpSecurity http) throws Exception {
http.authorizeExchange()
// URL that starts with / or /login/
.pathMatchers("/", "/login", "/js/**", "/images/**", "/css/**", "/h2-console/**").permitAll()
.anyExchange().authenticated().and().formLogin()
.authenticationManager(authManager)
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccesHandler("/findAllCustomers"));
return http.build();
}
}
WebFluxAuthManager class
#Component
public class WebFluxAuthManager implements ReactiveAuthenticationManager {
#Value("${gateway.url}")
private String gatewayUrl;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
// return is already authenticated
if (authentication.isAuthenticated()) {
return Mono.just(authentication);
}
String username = authentication.getName();
String password = authentication.getCredentials().toString();
LoginRequest loginRequest = new LoginRequest(username, password);
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
//todo modify to use webclient
HttpPost httpPost = new HttpPost(this.gatewayUrl + "/authenticate");
httpPost.setHeader("Content-type", "application/json");
String jsonReq = converObjectToJson(loginRequest);
StringEntity requestEntity = new StringEntity(jsonReq);
httpPost.setEntity(requestEntity);
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.OK.value()) {
HttpEntity entity = httpResponse.getEntity();
Header encodingHeader = entity.getContentEncoding();
Charset encoding = encodingHeader == null ? StandardCharsets.UTF_8
: Charsets.toCharset(encodingHeader.getValue());
// use org.apache.http.util.EntityUtils to read json as string
String jsonRes = EntityUtils.toString(entity, encoding);
LoginResponse loginResponse = converJsonToResponse(jsonRes);
Collection<? extends GrantedAuthority> authorities = loginResponse.getRoles().stream()
.map(item -> new SimpleGrantedAuthority(item)).collect(Collectors.toList());
return Mono.just(new UsernamePasswordAuthenticationToken(username, password, authorities));
} else {
throw new BadCredentialsException("Authentication Failed!!!");
}
} catch (RestClientException | ParseException | IOException e) {
throw new BadCredentialsException("Authentication Failed!!!", e);
} finally {
try {
if (httpClient != null)
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
In WebFluxAuthManager, I have access to the token, now I search a way to transfert it to a fragment.

Related

How to send JWT token to another Spring App

I have two spring apps: Users-App(login/registration, etc.) and main app. I need to access main app via JWT. How can I send token from users-app to main-app by using "Simple" Controller?
My Controller(users-app):
#Controller
#RequiredArgsConstructor
public class UserController {
private final UserService userService;
#Autowired
private UserRegistrationValidator userValidator;
#Autowired
private LoginValidator loginValidator;
#GetMapping("/")
public String startPage() {
return "redirect:/index";
}
#GetMapping("/index")
public String homePage() {
return "home";
}
#GetMapping("/users")
public String getUsers() {
return "redirect:http://localhost:8080/";
}
#GetMapping("/login")
public String loginPage(HttpServletRequest request, Model model) {
User user = new User();
model.addAttribute("user", user);
if (isCookiesExists(request)) return "redirect:/users";
return "login";
}
#PostMapping("/signin")
public String loginUser(#ModelAttribute("user") User user, BindingResult bindingResult) {
loginValidator.validate(user, bindingResult);
if (bindingResult.hasErrors()) {
return "login";
}
return "redirect:/users";
}
#GetMapping("/register")
public String registerUser(HttpServletRequest request, Model model) {
User user = new User();
model.addAttribute("my_user", user);
if (isCookiesExists(request)) return "redirect:/users";
return "register";
}
#PostMapping("/register")
public String saveUser(#ModelAttribute("my_user") User user, BindingResult bindingResult) {
userValidator.validate(user, bindingResult);
if (bindingResult.hasErrors()) {
return "register";
}
userService.saveUser(user);
return "redirect:/login";
}
private boolean isCookiesExists(HttpServletRequest request) {
if (request.getCookies() != null) {
List<String> auth = Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals("auth"))
.map(Cookie::getValue)
.filter(Objects::nonNull).collect(Collectors.toList());
return auth.size() != 0;
}
return false;
}
}
Creating tokens:
#Service
#RequiredArgsConstructor
#Transactional
#Slf4j
public class GetTokenServiceImpl implements GetTokenService {
private final PasswordEncoder passwordEncoder;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private UserService userService;
#Override
public String createToken(HttpServletRequest request, User user) {
Algorithm algorithm = Algorithm.HMAC512("my_secret_key_10210_oqpowqkq192199qkkwoxa");
return JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.withClaim(
"roles",
user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())
)
.sign(algorithm);
}
#Override
public String createRefreshToken(HttpServletRequest request, User user) {
Algorithm algorithm = Algorithm.HMAC512("my_secret_key_10210_oqpowqkq192199qkkwoxa");
return JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 60 * 60 * 10000))
.withIssuer(request.getRequestURL().toString())
.sign(algorithm);
}
#Override
public Map<String, String> getTokens(HttpServletRequest request, String username, String password) {
com.user.app.server.model.User myUser = userService.getUser(username);
if (myUser == null || password == null || !passwordEncoder.matches(password, myUser.getPassword()) ) {
log.error("Error logging in: {} ", "Bad Credentials error");
}
User user = (User) userDetailsService.loadUserByUsername(username);
String accessToken = createToken(request, user);
String refreshToken = createRefreshToken(request, user);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", accessToken);
tokens.put("refresh_token", refreshToken);
return tokens;
}
}
And my authorization filter:
#Slf4j
public class CustomAuthorizationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String header = "";
if (request.getServletPath().equals("/users")){
header = request.getHeader(AUTHORIZATION);
}
if (header != null && header.startsWith("Bearer ")) {
try {
String token = header.substring("Bearer ".length());
Algorithm algorithm = Algorithm.HMAC512("my_secret_key_10210_oqpowqkq192199qkkwoxa");
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String username = decodedJWT.getSubject();
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
stream(roles).forEach(s -> authorities.add(new SimpleGrantedAuthority(s)));
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (Exception exception) {
log.error("Error logging in: {}", exception.getMessage());
response.setHeader("error", exception.getMessage());
response.setStatus(FORBIDDEN.value());
Map<String, String> error = new HashMap<>();
error.put("error_message", exception.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
} else {
filterChain.doFilter(request, response);
}
}
}
Ps: Actually I did this by saving the token in cookies, but I didn't think this is a good practice. This is additional filter in users-app:
#Component
#RequiredArgsConstructor
public class CustomHeaderFilter implements Filter {
private final GetTokenService tokenService;
private Map<String, String> tokens = new HashMap<>();
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp =(HttpServletResponse) response;
if (req.getServletPath().equals("/signin")) {
tokens = tokenService
.getTokens(req, req.getParameter("username"), req.getParameter("password"));
}
if (req.getServletPath().equals("/users")) {
String value = "Bearer " + tokens.get("access_token");
resp.setHeader(AUTHORIZATION, value);
Cookie cookies = new Cookie("auth", tokens.get("access_token"));
cookies.setPath("/");
cookies.setMaxAge(1800);
resp.addCookie(cookies);
}
chain.doFilter(req, response);
}
}
And this is how I get from cookies(in main app):
#Slf4j
public class CustomAuthorizationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws IOException {
String header = request.getHeader(AUTHORIZATION);
Cookie[] cookies = request.getCookies();
if (cookies != null) {
String[] info = stream(cookies).map(Cookie::getValue).toArray(String[]::new);
header = "Bearer " + info[0];
}
try {
String token = header.substring("Bearer ".length());
Algorithm algorithm = Algorithm.HMAC512("my_secret_key_10210_oqpowqkq192199qkkwoxa");
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String username = decodedJWT.getSubject();
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
stream(roles).forEach(s -> authorities.add(new SimpleGrantedAuthority(s)));
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (Exception exception) {
log.error("Error logging in: {}", exception.getMessage());
response.setHeader("error", exception.getMessage());
response.setStatus(FORBIDDEN.value());
Map<String, String> error = new HashMap<>();
error.put("error_message", exception.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
}
}

Token alway not valid

In a spring boot reactive project, with r2dbc I try to add security with jwt
#RestController
public class UserController {
private final ReactiveUserDetailsServiceImpl userService;
private final TokenProvider jwtTokenUtil;
private final JWTReactiveAuthenticationManager authenticationManager;
...
#PostMapping(value="authorize")
public Mono<JWTToken> authorize(LoginUser loginVM) {
if (loginVM ==null || loginVM.getUsername().isEmpty() || loginVM.getPassword().isEmpty()) {
return Mono.error(new RuntimeException("Bad request"));
}
Authentication authenticationToken =
new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword());
Mono<Authentication> authentication = this.authenticationManager.authenticate(authenticationToken);
authentication.doOnError(throwable -> {
throw new BadCredentialsException("Bad crendentials");
});
ReactiveSecurityContextHolder.withAuthentication(authenticationToken);
return authentication.map(auth -> {
String jwt = jwtTokenUtil.createToken(auth);
return new JWTToken(jwt);
});
}
}
#Service
public class ReactiveUserDetailsServiceImpl implements ReactiveUserDetailsService, UserService {
#Autowired
private UserRepository userRepository;
#Override
public Mono<UserDetails> findByUsername(String username) {
return userRepository.findByUsername(username)
.filter(Objects::nonNull)
.switchIfEmpty(Mono.error(new BadCredentialsException(String.format("User %s not found in database", username))))
.map(this::createSpringSecurityUser);
}
private org.springframework.security.core.userdetails.User createSpringSecurityUser(User user) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(user.getRole());
grantedAuthorities.add(grantedAuthority);
return new org.springframework.security.core.userdetails.User(user.getUsername(),
user.getPassword(), grantedAuthorities);
}
public Flux<User> findAll(){
return userRepository.findAll();
}
}
#Component
public class TokenProvider {
private final long validityInMilliseconds = 3600000; // 1h
private static final String SALT_KEY = "123";
private String secretKey;
private final Base64.Encoder encoder = Base64.getEncoder();
private static final String AUTHORITIES_KEY = "auth";
private static final Logger logger = LogManager.getLogger(TokenProvider.class);
#PostConstruct
public void init() {
this.secretKey = encoder.encodeToString(SALT_KEY.getBytes(StandardCharsets.UTF_8));
}
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
Date validity = new Date(now + this.validityInMilliseconds);
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(SignatureAlgorithm.HS512, secretKey)
.setExpiration(validity)
.compact();
}
public Authentication getAuthentication(String token) {
if (StringUtils.isEmpty(token) || !validateToken(token)) {
throw new BadCredentialsException("Invalid token");
}
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
Collection<? extends GrantedAuthority> authorities
= Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
private boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.info("Invalid JWT signature.");
logger.trace("Invalid JWT signature trace: {}", e);
} catch (MalformedJwtException e) {
logger.info("Invalid JWT token.");
logger.trace("Invalid JWT token trace: {}", e);
} catch (ExpiredJwtException e) {
logger.info("Expired JWT token.");
logger.trace("Expired JWT token trace: {}", e);
} catch (UnsupportedJwtException e) {
logger.info("Unsupported JWT token.");
logger.trace("Unsupported JWT token trace: {}", e);
} catch (IllegalArgumentException e) {
logger.info("JWT token compact of handler are invalid.");
logger.trace("JWT token compact of handler are invalid trace: {}", e);
}
return false;
}
}
public class JWTReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private final ReactiveUserDetailsServiceImpl userService;
private final PasswordEncoder passwordEncoder;
private static final Logger logger = LogManager.getLogger(JWTReactiveAuthenticationManager.class);
public JWTReactiveAuthenticationManager(final PasswordEncoder passwordEncoder, final ReactiveUserDetailsServiceImpl userService) {
this.passwordEncoder = passwordEncoder;
this.userService = userService;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
if (authentication.isAuthenticated()) {
return Mono.just(authentication);
}
return Mono.just(authentication)
.switchIfEmpty(Mono.defer(this::raiseBadCredentials))
.cast(UsernamePasswordAuthenticationToken.class)
.flatMap(this::authenticateToken)
.publishOn(Schedulers.parallel())
.onErrorResume(e -> raiseBadCredentials())
.filter(u -> passwordEncoder.matches((String) authentication.getCredentials(), u.getPassword()))
.switchIfEmpty(Mono.defer(this::raiseBadCredentials))
.map(u -> new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), u.getAuthorities()));
}
private <T> Mono<T> raiseBadCredentials() {
return Mono.error(new BadCredentialsException("Invalid Credentials"));
}
private Mono<UserDetails> authenticateToken(final UsernamePasswordAuthenticationToken authenticationToken) {
String username = authenticationToken.getName();
logger.info("checking authentication for user " + username);
//todo change, it's not reactive
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
logger.info("authenticated user " + username + ", setting security context");
return userService.findByUsername(username);
}
return null;
}
}
#Configuration
#EnableWebFlux
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SpringSecurityWebFluxConfig {
private ReactiveUserDetailsServiceImpl userService;
private TokenProvider tokenUtil;
private static final String[] AUTH_WHITELIST = {
"/resources/**",
"/webjars/**",
"/authorize/**",
"/favicon.ico",};
public SpringSecurityWebFluxConfig(TokenProvider tokenUtil, ReactiveUserDetailsServiceImpl userService) {
this.tokenUtil = tokenUtil;
this.userService = userService;
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, UnauthorizedAuthenticationEntryPoint entryPoint) {
http.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable();
http
.exceptionHandling()
.authenticationEntryPoint(entryPoint)
.and()
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS)
.permitAll()
.pathMatchers("/users")
.permitAll()
.and()
.addFilterAt(webFilter(), SecurityWebFiltersOrder.AUTHORIZATION)
.authorizeExchange()
.pathMatchers(AUTH_WHITELIST).permitAll()
.anyExchange().authenticated();
return http.build();
}
#Bean
public AuthenticationWebFilter webFilter() {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(repositoryReactiveAuthenticationManager());
authenticationWebFilter.setAuthenticationConverter(new TokenAuthenticationConverter(tokenUtil));
authenticationWebFilter.setRequiresAuthenticationMatcher(new JWTHeadersExchangeMatcher());
authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
return authenticationWebFilter;
}
#Bean
public JWTReactiveAuthenticationManager repositoryReactiveAuthenticationManager() {
JWTReactiveAuthenticationManager repositoryReactiveAuthenticationManager = new JWTReactiveAuthenticationManager(passwordEncoder(), userService);
return repositoryReactiveAuthenticationManager;
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
I try to connect with curl with this line
curl -X POST localhost:8889/authorize -u admin:123
I see in debug then validateToken receive: WRtaW46cGVybWFjb24=
Error trapped is: MalformedJwtException
Don't understand why
If i try to call directly /users (not supposed to add security) i get
curl -X GET http://localhost:8889/users
I get
Error: SEVERITY_LOCALIZED=ERROR, SEVERITY_NON_LOCALIZED=ERROR,
CODE=42601, MESSAGE=syntax error at or near ".", POSITION=12,
FILE=scan.l, LINE=1128, ROUTINE=scanner_yyerror

How to add a custom OpenId Filter in a Spring boot application?

I am trying to implement the backend side of an OpenId Connect authentication. It is a stateless API so I added a filter that handles the Bearer token.
I have created the OpenIdConnect Filter that handles the Authentication and added it in a WebSecurityConfigurerAdapter.
public class OpenIdConnectFilter extends
AbstractAuthenticationProcessingFilter {
#Value("${auth0.clientId}")
private String clientId;
#Value("${auth0.issuer}")
private String issuer;
#Value("${auth0.keyUrl}")
private String jwkUrl;
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
public OpenIdConnectFilter() {
super("/connect/**");
setAuthenticationManager(new NoopAuthenticationManager());
}
#Bean
public FilterRegistrationBean registration(OpenIdConnectFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
try {
Authentication authentication = tokenExtractor.extract(request);
String accessToken = (String) authentication.getPrincipal();
String kid = JwtHelper.headers(accessToken)
.get("kid");
final Jwt tokenDecoded = JwtHelper.decodeAndVerify(accessToken, verifier(kid));
final Map<String, Object> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
Set<String> scopes = new HashSet<String>(Arrays.asList(((String) authInfo.get("scope")).split(" ")));
int expires = (Integer) authInfo.get("exp");
OpenIdToken openIdToken = new OpenIdToken(accessToken, scopes, Long.valueOf(expires), authInfo);
final OpenIdUserDetails user = new OpenIdUserDetails((String) authInfo.get("sub"), "Test", openIdToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (final Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) || !claims.get("azp").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}
private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
Here is security configuration:
#Configuration
#EnableWebSecurity
public class OpenIdConnectWebServerConfig extends
WebSecurityConfigurerAdapter {
#Bean
public OpenIdConnectFilter myFilter() {
final OpenIdConnectFilter filter = new OpenIdConnectFilter();
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.antMatcher("/connect/**").authorizeRequests()
.antMatchers(HttpMethod.GET, "/connect/public").permitAll()
.antMatchers(HttpMethod.GET, "/connect/private").authenticated()
.antMatchers(HttpMethod.GET, "/connect/private-
messages").hasAuthority("read:messages")
.antMatchers(HttpMethod.GET, "/connect/private-
roles").hasAuthority("read:roles")
.and()
.addFilterBefore(myFilter(),
UsernamePasswordAuthenticationFilter.class);
}
Rest endpoints looks like following:
#RequestMapping(value = "/connect/public", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String publicEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You DO NOT need to be authenticated to
call /api/public.")
.toString();
}
#RequestMapping(value = "/connect/private", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String privateEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You can see this because you are
Authenticated.")
.toString();
}
If I remove completely the filter for configuration and also the #Bean definition, the configuration works as expected: /connect/public is accessible, while /connect/private is forbidden.
If I keep the #Bean definition and add it in filter chain the response returns a Not Found status for requests both on /connect/public and /connect/private:
"timestamp": "18.01.2019 09:46:11",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/
When debugging I noticed that filter is processing the token and returns an implementation of Authentication.
Is the filter properly added in filter chain and in correct position?
Why is the filter invoked also on /connect/public path when this is supposed to be public. Is it applied to all paths matching super("/connect/**") call?
Why is it returning the path as "/" when the request is made at /connect/private
Seems that is something wrong with the filter, cause every time it is applied, the response is messed up.

Spring Webflux OAuth 2 resource server

I have a Spring OAuth 2 server based on Spring Boot 1.5 (Spring Security v4) which generates customized tokens and a few resource servers who communicate with this authorization server, making use of /oauth/check_token endpoint by configuration of RemoteTokenServices.
All the logic related to storing/retrieving tokens on Authorization server side is done with JdbcTokenStore.
I am building a new Spring Boot 2 application which is build with Spring webflux module and trying to implement client_credentials flow with existing Authorization Server using Spring Security 5.1.1.
I found that support for resource servers was added in 5.1.0.RC1 (https://spring.io/blog/2018/08/21/spring-security-5-1-0-rc1-released#oauth2-resource-servers) and updated in 5.1.0.RC2 (https://spring.io/blog/2018/09/10/spring-security-5-1-0-rc2-released#oauth2-resource-server) but looks like it's only possible to configure it with JWT support.
I might be messing up with concepts here but looking for more info and a way to configure all these components together.
I'm in same situation as you.I solve that problem in next way, maybe it can help you:
spring-boot-starter-parent.version: 2.1.1
spring-cloud-dependencies.version: Greenwich.R1
Security configuration:
#EnableWebFluxSecurity
public class SecurityConfig {
#Autowired
private ReactiveAuthenticationManager manager; //custom implementation
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/role").hasRole("ADMIN")
.pathMatchers("/test").access(new HasScope("server")) //custom implementation
.anyExchange().authenticated()
.and()
.httpBasic().disable()
.oauth2ResourceServer()
.jwt()
.authenticationManager(manager)
.and().and()
.build();
}
}
ReactiveAuthorizationManager (HasScope) implementation:
Helper which allow search for scopes in authentication object
public class HasScope implements ReactiveAuthorizationManager<AuthorizationContext> {
public HasScope(String...scopes) {
this.scopes = Arrays.asList(scopes);
}
private final Collection<String> scopes;
#Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
return authentication
.flatMap(it -> {
OAuth2Authentication auth = (OAuth2Authentication) it;
Set<String> requestScopes = auth.getOAuth2Request().getScope();
boolean allow = requestScopes.containsAll(scopes);
return Mono.just(new AuthorizationDecision(allow));
});
}
}
ReactiveAuthenticationManager implementation:
That is the main component in configuration which create OAuth2Authentication. There is a problem with response for wrong access_token, it returns only status code without body response.
#Component
public class ReactiveAuthenticationManagerImpl implements ReactiveAuthenticationManager {
private final ResourceServerProperties sso;
private final WebClient.Builder webClient;
private final ObjectMapper objectMapper;
private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
public ReactiveAuthenticationManagerImpl(ResourceServerProperties sso,
#Qualifier("loadBalancedWebClient") WebClient.Builder webClient, ObjectMapper objectMapper) {
this.sso = sso;
this.webClient = webClient;
this.objectMapper = objectMapper;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.cast(BearerTokenAuthenticationToken.class)
.flatMap(it -> getMap(it.getToken()))
.flatMap(result -> Mono.just(extractAuthentication(result)));
}
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
Object principal = getPrincipal(map);
OAuth2Request request = getRequest(map);
List<GrantedAuthority> authorities = authoritiesExtractor.extractAuthorities(map);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
private Object getPrincipal(Map<String, Object> map) {
if (map.containsKey("principal")) {
try {
//that is the case for user authentication
return objectMapper.convertValue(map.get("principal"), UserPrincipal.class);
} catch (IllegalArgumentException ex) {
//that is the case for client authentication
return objectMapper.convertValue(map.get("principal"), String.class);
}
}
return null;
}
#SuppressWarnings({"unchecked"})
private OAuth2Request getRequest(Map<String, Object> map) {
Map<String, Object> request = (Map<String, Object>) map.get("oauth2Request");
String clientId = (String) request.get("clientId");
Set<String> scope = new LinkedHashSet<>(request.containsKey("scope") ?
(Collection<String>) request.get("scope") : Collections.emptySet());
return new OAuth2Request(null, clientId, null, true, new HashSet<>(scope),
null, null, null, null);
}
private Mono<Map<String, Object>> getMap(String accessToken) {
String uri = sso.getUserInfoUri();
return webClient.build().get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + accessToken)
.exchange()
.flatMap(it -> it.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {}))
.onErrorMap(InvalidTokenException.class, mapper -> new InvalidTokenException("Invalid token: " + accessToken));
}

Spring Boot Social Login and Local OAuth2-Server

I'm currently working on a Spring Boot-Application with OAuth2-Authentication. I have a local OAuth2-Server where I receive a token when posting username and password of the local database against in my case http://localhost:8080/v1/oauth/token using Spring Boot's UserDetails and UserService. Everything works fine and nice.
But now I want to enhance my program with Facebook social login and want either log in to my local OAuth2-Server or using the external Facebook-Server. I checked out the Spring Boot example https://spring.io/guides/tutorials/spring-boot-oauth2/ and adapted the idea of an SSO-Filter. Now I can login using my Facebook client and secret id, but I cannot access my restricted localhost-sites.
What I want is that the Facebook-Token "behaves" the same way as the locally generated tokens by for instance being part of my local token storage. I checked out several tutorials and other Stackoverflow questions but with no luck. Here is what I have so far with a custom Authorization-Server and I think I'm still missing something very basic to get the link between external Facebook- and internal localhost-Server:
#Configuration
public class OAuth2ServerConfiguration {
private static final String SERVER_RESOURCE_ID = "oauth2-server";
#Autowired
private TokenStore tokenStore;
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
protected class ClientResources {
#NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
#NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
#Configuration
#EnableResourceServer
#EnableOAuth2Client
protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Value("${pia.requireauth}")
private boolean requireAuth;
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore).resourceId(SERVER_RESOURCE_ID);
}
#Autowired
OAuth2ClientContext oauth2ClientContext;
#Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
#ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
#Override
public void configure(HttpSecurity http) throws Exception {
if (!requireAuth) {
http.antMatcher("/**").authorizeRequests().anyRequest().permitAll();
} else {
http.antMatcher("/**").authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest().authenticated().and()
.exceptionHandling().and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
}
}
#Configuration
#EnableAuthorizationServer
protected class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
#Value("${pia.oauth.tokenTimeout:3600}")
private int expiration;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
#Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
// password encryptor
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {
configurer.authenticationManager(authenticationManager).tokenStore(tokenStore).approvalStoreDisabled();
configurer.userDetailsService(userDetailsService);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("pia").secret("alphaport").accessTokenValiditySeconds(expiration)
.authorities("ROLE_USER").scopes("read", "write").authorizedGrantTypes("password", "refresh_token")
.resourceIds(SERVER_RESOURCE_ID);
}
}
}
Any help and/or examples covering this issue greatly appreciated! :)
One possible solution is to implement the Authentication Filter and Authentication Provider.
In my case I've implemented an OAuth2 authentication and also permit the user to access some endpoints with facebook access_token
The Authentication Filter looks like this:
public class ServerAuthenticationFilter extends GenericFilterBean {
private BearerAuthenticationProvider bearerAuthenticationProvider;
private FacebookAuthenticationProvider facebookAuthenticationProvider;
public ServerAuthenticationFilter(BearerAuthenticationProvider bearerAuthenticationProvider,
FacebookAuthenticationProvider facebookAuthenticationProvider) {
this.bearerAuthenticationProvider = bearerAuthenticationProvider;
this.facebookAuthenticationProvider = facebookAuthenticationProvider;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
Optional<String> authorization = Optional.fromNullable(httpRequest.getHeader("Authorization"));
try {
AuthType authType = getAuthType(authorization.get());
if (authType == null) {
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
String strToken = authorization.get().split(" ")[1];
if (authType == AuthType.BEARER) {
if (strToken != null) {
Optional<String> token = Optional.of(strToken);
logger.debug("Trying to authenticate user by Bearer method. Token: " + token.get());
processBearerAuthentication(token);
}
} else if (authType == AuthType.FACEBOOK) {
if (strToken != null) {
Optional<String> token = Optional.of(strToken);
logger.debug("Trying to authenticate user by Facebook method. Token: " + token.get());
processFacebookAuthentication(token);
}
}
logger.debug(getClass().getSimpleName() + " is passing request down the filter chain.");
chain.doFilter(request, response);
} catch (InternalAuthenticationServiceException internalAuthenticationServiceException) {
SecurityContextHolder.clearContext();
logger.error("Internal Authentication Service Exception", internalAuthenticationServiceException);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (AuthenticationException authenticationException) {
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
} catch (Exception e) {
SecurityContextHolder.clearContext();
e.printStackTrace();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}
private AuthType getAuthType(String value) {
if (value == null)
return null;
String[] basicSplit = value.split(" ");
if (basicSplit.length != 2)
return null;
if (basicSplit[0].equalsIgnoreCase("bearer"))
return AuthType.BEARER;
if (basicSplit[0].equalsIgnoreCase("facebook"))
return AuthType.FACEBOOK;
return null;
}
private void processBearerAuthentication(Optional<String> token) {
Authentication resultOfAuthentication = tryToAuthenticateWithBearer(token);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private void processFacebookAuthentication(Optional<String> token) {
Authentication resultOfAuthentication = tryToAuthenticateWithFacebook(token);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private Authentication tryToAuthenticateWithBearer(Optional<String> token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token,
null);
return tryToAuthenticateBearer(requestAuthentication);
}
private Authentication tryToAuthenticateWithFacebook(Optional<String> token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token,
null);
return tryToAuthenticateFacebook(requestAuthentication);
}
private Authentication tryToAuthenticateBearer(Authentication requestAuthentication) {
Authentication responseAuthentication = bearerAuthenticationProvider.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException(
"Unable to Authenticate for provided credentials.");
}
logger.debug("Application successfully authenticated by bearer method.");
return responseAuthentication;
}
private Authentication tryToAuthenticateFacebook(Authentication requestAuthentication) {
Authentication responseAuthentication = facebookAuthenticationProvider.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException(
"Unable to Authenticate for provided credentials.");
}
logger.debug("Application successfully authenticated by facebook method.");
return responseAuthentication;
}
}
This, filters Authorization headers, identifies whether they are facebook or bearer and then directs to specific provider.
The Facebook Provider looks like this:
public class FacebookAuthenticationProvider implements AuthenticationProvider {
#Value("${config.oauth2.facebook.resourceURL}")
private String facebookResourceURL;
private static final String PARAMETERS = "fields=name,email,gender,picture";
#Autowired
FacebookUserRepository facebookUserRepository;
#Autowired
UserRoleRepository userRoleRepository;
#SuppressWarnings({ "rawtypes", "unchecked" })
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
Optional<String> token = auth.getPrincipal() instanceof Optional ? (Optional) auth.getPrincipal() : null;
if (token == null || !token.isPresent() || token.get().isEmpty())
throw new BadCredentialsException("Invalid Grants");
SocialResourceUtils socialResourceUtils = new SocialResourceUtils(facebookResourceURL, PARAMETERS);
SocialUser socialUser = socialResourceUtils.getResourceByToken(token.get());
if (socialUser != null && socialUser.getId() != null) {
User user = findOriginal(socialUser.getId());
if (user == null)
throw new BadCredentialsException("Authentication failed.");
Credentials credentials = new Credentials();
credentials.setId(user.getId());
credentials.setUsername(user.getEmail());
credentials.setName(user.getName());
credentials.setRoles(parseRoles(user.translateRoles()));
credentials.setToken(token.get());
return new UsernamePasswordAuthenticationToken(credentials, credentials.getId(),
parseAuthorities(getUserRoles(user.getId())));
} else
throw new BadCredentialsException("Authentication failed.");
}
protected User findOriginal(String id) {
FacebookUser facebookUser = facebookUserRepository.findByFacebookId(facebookId);
return null == facebookUser ? null : userRepository.findById(facebookUser.getUserId()).get();
}
protected List<String> getUserRoles(String id) {
List<String> roles = new ArrayList<>();
userRoleRepository.findByUserId(id).forEach(applicationRole -> roles.add(applicationRole.getRole()));
return roles;
}
private List<Roles> parseRoles(List<String> strRoles) {
List<Roles> roles = new ArrayList<>();
for(String strRole : strRoles) {
roles.add(Roles.valueOf(strRole));
}
return roles;
}
private Collection<? extends GrantedAuthority> parseAuthorities(Collection<String> roles) {
if (roles == null || roles.size() == 0)
return Collections.emptyList();
return roles.stream().map(role -> (GrantedAuthority) () -> "ROLE_" + role).collect(Collectors.toList());
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
The FacebookUser only makes a reference to the Local User Id and the Facebook Id (this is the link between facebook and our application).
This SocialResourceUtils is used to get the facebook user information via facebook API (using the method getResourceByToken). The facebook resource url is setted on application.properties (config.oauth2.facebook.resourceURL). This method is basically:
public SocialUser getResourceByToken(String token) {
RestTemplate restTemplate = new RestTemplate();
String authorization = token;
JsonNode response = null;
try {
response = restTemplate.getForObject(accessUrl + authorization, JsonNode.class);
} catch (RestClientException e) {
throw new BadCredentialsException("Authentication failed.");
}
return buildSocialUser(response);
}
The Bearer Provider is your local Authentication, you can make your own, or use the springboot defaults, use other authentication methods, idk (I will not put my implementation here, thats by you).
And finally you need to make your Web Security Configurer:
#ConditionalOnProperty("security.basic.enabled")
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private BearerAuthenticationProvider bearerAuthenticationProvider;
#Autowired
private FacebookAuthenticationProvider facebookAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.addFilterBefore(new ServerAuthenticationFilter(bearerAuthenticationProvider,
facebookAuthenticationProvider), BasicAuthenticationFilter.class);
}
}
Notice that it has the annotation ConditionalOnProperty to enable/disable on properties security.basic.enabled. The #EnableGlobalMethodSecurity(prePostEnabled = true) enables the usage of the annotation #PreAuthorize which enables us to protect endpoints by roles for example (using #PreAuthorize("hasRole ('ADMIN')") over an endpoint, to allow acces only to admins)
This code needs many improvements, but I hope I have helped.

Resources