Spring reactive security - spring-boot

I am trying for reactive security and the unauthenticated calls are not going to auth manager.
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig{
#Autowired
private WebAuthenticationManager authenticationManager;
#Autowired
private ServerSecurityContextRepository securityContextRepository;
private static final String[] AUTH_WHITELIST = {
"/login/**",
"/logout/**",
"/authorize/**",
"/favicon.ico",
};
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.exceptionHandling().authenticationEntryPoint((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
}).and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange().pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers(AUTH_WHITELIST).permitAll()
.anyExchange().authenticated().and().build();
}
#Bean
public PBKDF2Encoder passwordEncoder() {
return new PBKDF2Encoder();
}
}
WebAuthentication Manager,
#Component
public class WebAuthenticationManager implements ReactiveAuthenticationManager {
#Autowired
private JWTUtil jwtUtil;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null && jwtUtil.validateToken(authToken)) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> rolesMap = claims.get("role", List.class);
List<Role> roles = new ArrayList<>();
for (String rolemap : rolesMap) {
roles.add(Role.valueOf(rolemap));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList())
);
return Mono.just(auth);
} else {
return Mono.empty();
}
}
}
Here, I have registered my WebAuthentication manager in Securityconfig. But, still the unauthenticated calls are not going to the WebAuthenticationManager.
It is expected to go to AuthenticationManager when the protected URL's are hit. For ex,
http://localhost:8080/api/v1/user.
Not sure, why the calls are not going to AuthManager.
In non reactive, we have OncePerRequestFilter and the auth is being taken care over there. Not sure, how to implement the same for reactive.

You disabled all authentication mechanisms hence there is nothing calling your authentication manager. As you mentioned, you can implement authentication flow through filters.
Sample implementation of authentication filter:
#Bean
public AuthenticationWebFilter webFilter() {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter());
authenticationWebFilter.setRequiresAuthenticationMatcher(serverWebExchangeMatcher());
authenticationWebFilter.setSecurityContextRepository(NoOpServerSecurityContextRepository.getInstance());
return authenticationWebFilter;
}
Then add this filter to ServerHttpSecurity: http.addFilterBefore(webFilter(),SecurityWebFiltersOrder.HTTP_BASIC)
Then finally your authentication manager will be called.
You must provide few additional things to make it working.
Matcher to check if Authorization header is added to request:
#Bean
public ServerWebExchangeMatcher serverWebExchangeMatcher() {
return exchange -> {
Mono<ServerHttpRequest> request = Mono.just(exchange).map(ServerWebExchange::getRequest);
return request.map(ServerHttpRequest::getHeaders)
.filter(h -> h.containsKey(HttpHeaders.AUTHORIZATION))
.flatMap($ -> ServerWebExchangeMatcher.MatchResult.match())
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
};
}
Token converter responsible for getting token from request and preparing basic AbstractAuthenticationToken
#Bean
public ServerAuthenticationConverter tokenAuthenticationConverter() {
return exchange -> Mono.justOrEmpty(exchange)
.map(e -> getTokenFromRequest(e))
.filter(token -> !StringUtils.isEmpty(token))
.map(token -> getAuthentication(token));
}
I intentionally omitted implementation of getTokenFromRequest and getAuthentication because there is a lot of examples available.

Related

How to response custom json body on unauthorized requests while implementing custom authentication manager in webflux

I was trying to implement custom JWT token authentication while i am also handling global exceptions to customize response body for each type of exceptions. Everything is working fine except I would like to return custom json response when an unauthorized request is received instead of just 401 status code.
Below is my implementation for JwtServerAuthenticationConverter and JwtAuthenticationManager.
#Component
public class JwtServerAuthenticationConverter implements ServerAuthenticationConverter {
private static final String AUTH_HEADER_VALUE_PREFIX = "Bearer ";
#Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange)
.flatMap(serverWebExchange -> Mono.justOrEmpty(
serverWebExchange
.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION)
)
)
.filter(header -> !header.trim().isEmpty() && header.trim().startsWith(AUTH_HEADER_VALUE_PREFIX))
.map(header -> header.substring(AUTH_HEADER_VALUE_PREFIX.length()))
.map(token -> new UsernamePasswordAuthenticationToken(token, token))
;
}
}
#Component
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
private final JWTConfig jwtConfig;
private final ObjectMapper objectMapper;
public JwtAuthenticationManager(JWTConfig jwtConfig, ObjectMapper objectMapper) {
this.jwtConfig = jwtConfig;
this.objectMapper = objectMapper;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.map(auth -> JWTHelper.loadAllClaimsFromToken(auth.getCredentials().toString(), jwtConfig.getSecret()))
.onErrorResume(throwable -> Mono.error(new JwtException("Unauthorized")))
.map(claims -> objectMapper.convertValue(claims, JWTUserDetails.class))
.map(jwtUserDetails ->
new UsernamePasswordAuthenticationToken(
jwtUserDetails,
authentication.getCredentials(),
jwtUserDetails.getGrantedAuthorities()
)
)
;
}
}
And below is my global exception handling which is working absolutely fine except the case where webflux return 401 from JwtServerAuthenticationConverter convert method.
#Configuration
#Order(-2)
public class ExceptionHandler implements WebExceptionHandler {
#Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
exchange.getResponse().getHeaders().set("Content-Type", MediaType.APPLICATION_JSON_VALUE);
return buildErrorResponse(ex)
.flatMap(
r -> r.writeTo(exchange, new HandlerStrategiesResponseContext(HandlerStrategies.withDefaults()))
);
}
private Mono<ServerResponse> buildErrorResponse(Throwable ex) {
if (ex instanceof RequestEntityValidationException) {
return ServerResponse.badRequest().contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse(ex.getMessage())),
ErrorResponse.class
);
} else if (ex instanceof ResponseStatusException) {
ResponseStatusException exception = (ResponseStatusException) ex;
if (exception.getStatus().value() == 404) {
return ServerResponse.status(HttpStatus.NOT_FOUND).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Resource not found - 404")),
ErrorResponse.class
);
} else if (exception.getStatus().value() == 400) {
return ServerResponse.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Unable to parse request body - 400")),
ErrorResponse.class
);
}
} else if (ex instanceof JwtException) {
return ServerResponse.status(HttpStatus.UNAUTHORIZED).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse(ex.getMessage())),
ErrorResponse.class
);
}
ex.printStackTrace();
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Internal server error - 500")),
ErrorResponse.class
);
}
}
#RequiredArgsConstructor
class HandlerStrategiesResponseContext implements ServerResponse.Context {
private final HandlerStrategies handlerStrategies;
#Override
public List<HttpMessageWriter<?>> messageWriters() {
return this.handlerStrategies.messageWriters();
}
#Override
public List<ViewResolver> viewResolvers() {
return this.handlerStrategies.viewResolvers();
}
}
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http,
ReactiveAuthenticationManager jwtAuthenticationManager,
ServerAuthenticationConverter jwtAuthenticationConverter
) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(jwtAuthenticationConverter);
return http
.authorizeExchange()
.pathMatchers("/auth/login", "/auth/logout").permitAll()
.anyExchange().authenticated()
.and()
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.httpBasic()
.disable()
.csrf()
.disable()
.formLogin()
.disable()
.logout()
.disable()
.build();
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
So when i am hitting it with an invalid JWT token in header. This got handled by my ExceptioHandler class and I got below output which is great.
But when i hit it with empty jwt token I got this.
Now i would like to return the same body which i am returning in the case of invalid JWT token. but the problem is when empty token is provided its not even falling in handle method of ExceptionHandler class. thats why its not in my control like i did for JwtException in the same class. How could i do that please help?
I sort it out myself.
webflux provides ServerAuthenticationFailureHandler to handle custom response for that but unfortunately ServerAuthenticationFailureHandler not works and its a known issue so i created a failure route and write my custom response in it and setup login page.
.formLogin()
.loginPage("/auth/failed")
.and()
.andRoute(path("/auth/failed").and(accept(MediaType.APPLICATION_JSON)), (serverRequest) ->
ServerResponse
.status(HttpStatus.UNAUTHORIZED)
.body(
Mono.just(new ErrorResponse("Unauthorized")),
ErrorResponse.class
)
);

How to extract custom Principal in OAuth2 Resource Server?

I'm using Keycloak as my OAuth2 Authorization Server and I configured an OAuth2 Resource Server for Multitenancy following this official example on GitHub.
The current Tenant is resolved considering the Issuer field of the JWT token.
Hence the token is verified against the JWKS exposed at the corresponding OpenID Connect well known endpoint.
This is my Security Configuration:
#EnableWebSecurity
#RequiredArgsConstructor
#EnableAutoConfiguration(exclude = UserDetailsServiceAutoConfiguration.class)
public class OrganizationSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final TenantService tenantService;
private List<Tenant> tenants;
#PostConstruct
public void init() {
this.tenants = this.tenantService.findAllWithRelationships();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.authenticationManagerResolver(new MultiTenantAuthenticationManagerResolver(this.tenants));
}
}
and this is my custom AuthenticationManagerResolver:
public class MultiTenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private final AuthenticationManagerResolver<HttpServletRequest> resolver;
private List<Tenant> tenants;
public MultiTenantAuthenticationManagerResolver(List<Tenant> tenants) {
this.tenants = tenants;
List<String> trustedIssuers = this.tenants.stream()
.map(Tenant::getIssuers)
.flatMap(urls -> urls.stream().map(URL::toString))
.collect(Collectors.toList());
this.resolver = new JwtIssuerAuthenticationManagerResolver(trustedIssuers);
}
#Override
public AuthenticationManager resolve(HttpServletRequest context) {
return this.resolver.resolve(context);
}
}
Now, because of the design of org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver
which is private, the only way I can think in order to extract a custom principal is to reimplement everything that follows:
TrustedIssuerJwtAuthenticationManagerResolver
the returned AuthenticationManager
the AuthenticationConverter
the CustomAuthenticationToken which extends JwtAuthenticationToken
the CustomPrincipal
To me it seems a lot of Reinventing the wheel, where my only need would be to have a custom Principal.
The examples that I found don't seem to suit my case since they refer to OAuth2Client or are not tought for Multitenancy.
https://www.baeldung.com/spring-security-oauth-principal-authorities-extractor
How to extend OAuth2 principal
Do I really need to reimplement all such classes/interfaes or is there a smarter approach?
This is how I did it, without reimplementing a huge amount of classes. This is without using a JwtAuthenticationToken however.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver()));
}
#Bean
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() {
List<String> issuers = ... // get this from list of tennants or config, whatever
Predicate<String> trustedIssuer = issuers::contains;
Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
AuthenticationManagerResolver<String> resolver = (String issuer) -> {
if (trustedIssuer.test(issuer)) {
return authenticationManagers.computeIfAbsent(issuer, k -> {
var jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
var provider = new JwtAuthenticationProvider(jwtDecoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationService::loadUserByJwt);
return provider::authenticate;
});
}
return null;
};
return new JwtIssuerAuthenticationManagerResolver(resolver);
}
}
#Service
public class JwtAuthenticationService {
public AbstractAuthenticationToken loadUserByJwt(Jwt jwt) {
UserDetails userDetails = ... // or your choice of principal
List<GrantedAuthority> authorities = ... // extract from jwt or db
...
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
}
}

Spring boot webflux security with JWT token

Trying to setup JWT token based auth with Spring boot webflux.
Spring boot version :- 2.3.0.BUILD-SNAPSHOT
Tech Stacks:- Angular 9, Spring boot 2.3.0.BUILD-SNAPSHOT, Spring security, Spring security JWT
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
GUI is angular 9 based and using form based authentication.
Need JWT for call coming from angular and also call coming to the API directly.
WebSecurityConfig,
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private ServerSecurityContextRepository securityContextRepository;
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.exceptionHandling().authenticationEntryPoint((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
}).and().csrf().disable().formLogin().disable().httpBasic().disable()
.authenticationManager(authenticationManager).securityContextRepository(securityContextRepository)
.authorizeExchange().pathMatchers(HttpMethod.OPTIONS).permitAll().pathMatchers("/login").permitAll()
.anyExchange().authenticated().and().build();
}
#Bean
public PBKDF2Encoder passwordEncoder() {
return new PBKDF2Encoder();
}
}
PBKDF2Encoder,
#Component
public class PBKDF2Encoder implements PasswordEncoder {
#Value("${springbootwebfluxjjwt.password.encoder.secret}")
private String secret;
#Value("${springbootwebfluxjjwt.password.encoder.iteration}")
private Integer iteration;
#Value("${springbootwebfluxjjwt.password.encoder.keylength}")
private Integer keylength;
/**
* More info (https://www.owasp.org/index.php/Hashing_Java)
*
* #param cs password
* #return encoded password
*/
#Override
public String encode(CharSequence cs) {
try {
byte[] result = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
.generateSecret(
new PBEKeySpec(cs.toString().toCharArray(), secret.getBytes(), iteration, keylength))
.getEncoded();
return Base64.getEncoder().encodeToString(result);
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
throw new RuntimeException(ex);
}
}
#Override
public boolean matches(CharSequence cs, String string) {
return encode(cs).equals(string);
}
}
AuthenticationManager,
#Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
#Autowired
private JWTUtil jwtUtil;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null && jwtUtil.validateToken(authToken)) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> rolesMap = claims.get("role", List.class);
List<Role> roles = new ArrayList<>();
for (String rolemap : rolesMap) {
roles.add(Role.valueOf(rolemap));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList())
);
return Mono.just(auth);
} else {
return Mono.empty();
}
}
}
SecurityContextRepository,
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
throw new UnsupportedOperationException("Not supported yet.");
}
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String authToken = authHeader.substring(7);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.authenticationManager.authenticate(auth).map((authentication) -> {
return new SecurityContextImpl(authentication);
});
} else {
return Mono.empty();
}
}
}
Is this the correct approach? Is there any better approach?
I've faced same problems, and after lot of research I've made a fully 100% functional demo project implementing webflux + webflux-security + others...
you can find the complete implementation here: https://github.com/eriknyk/webflux-jwt-security-demo
the implementation contains:
Spring webflux
Spring security implemented with JWT + validation layer
User register demo endpoint
User authentication endpoint
Model to dto mapping (using mapstruct)
User R2db with Postgresql repository impl
User validation in spring security layer, according to the user record in db

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