Angular 2 Spring Security CSRF Token - rest

Hi Everyone I'm having trouble setting up a security solution for my app!!
So I have a REST API Backend which runs at http://localhost:51030 and developed with Spring Framework, and for the front side I have an Angular 2 application (the latest version A.K.A. Angular 4) which runs at http://localhost:4200.
I have set the CORS configuration in the backend as seen below:
public class CORSFilter implements Filter
{
// The list of domains allowed to access the server
private final List<String> allowedOrigins = Arrays.asList("http://localhost:4200", "http://127.0.0.1:4200");
public void destroy()
{
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
{
// Lets make sure that we are working with HTTP (that is, against HttpServletRequest and HttpServletResponse objects)
if (req instanceof HttpServletRequest && res instanceof HttpServletResponse)
{
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// Access-Control-Allow-Origin
String origin = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", allowedOrigins.contains(origin) ? origin : "");
response.setHeader("Vary", "Origin");
// Access-Control-Max-Age
response.setHeader("Access-Control-Max-Age", "3600");
// Access-Control-Allow-Credentials
response.setHeader("Access-Control-Allow-Credentials", "true");
// Access-Control-Allow-Methods
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
// Access-Control-Allow-Headers
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, " + CSRF.REQUEST_HEADER_NAME); // + CSRF.REQUEST_HEADER_NAME
}
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig)
{
}
}
Using this configuration only works fine, I can execute requests from the angular app to the spring back and get response and do anything.
But when I try to set up CSRF security solution nothing works.
This is the CSRF and Security configuration setted up in the backend:
public class CSRF
{
/**
* The name of the cookie with the CSRF token sent by the server as a response.
*/
public static final String RESPONSE_COOKIE_NAME = "XSRF-TOKEN"; //CSRF-TOKEN
/**
* The name of the header carrying the CSRF token, expected in CSRF-protected requests to the server.
*/
public static final String REQUEST_HEADER_NAME = "X-XSRF-TOKEN"; //X-CSRF-TOKEN
// In Angular the CookieXSRFStrategy looks for a cookie called XSRF-TOKEN
// and sets a header named X-XSRF-TOKEN with the value of that cookie.
// The server must do its part by setting the initial XSRF-TOKEN cookie
// and confirming that each subsequent state-modifying request includes
// a matching XSRF-TOKEN cookie and X-XSRF-TOKEN header.
}
public class CSRFTokenResponseCookieBindingFilter extends OncePerRequestFilter
{
protected static final String REQUEST_ATTRIBUTE_NAME = "_csrf";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException
{
CsrfToken token = (CsrfToken) request.getAttribute(REQUEST_ATTRIBUTE_NAME);
Cookie cookie = new Cookie(CSRF.RESPONSE_COOKIE_NAME, token.getToken());
cookie.setPath("/");
response.addCookie(cookie);
filterChain.doFilter(request, response);
}
}
#Configuration
public class Conf extends WebMvcConfigurerAdapter
{
#Bean
public CORSFilter corsFilter()
{
return new CORSFilter();
}
#Override
public void addViewControllers(ViewControllerRegistry registry)
{
registry.addViewController("/login");
registry.addViewController("/logout");
}
}
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
#Autowired
private RESTLogoutSuccessHandler logoutSuccessHandler;
#Resource
private CORSFilter corsFilter;
#Autowired
private DataSource dataSource;
#Autowired
public void globalConfig(AuthenticationManagerBuilder auth) throws Exception
{
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select login as principal, password as credentials, true from user where login = ?")
.authoritiesByUsernameQuery("select login as principal, profile as role from user where login = ?")
.rolePrefix("ROLE_");
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
//csrf is disabled for the moment
//http.csrf().disable();
//authorized requests
http.authorizeRequests()
.antMatchers("/api/users/**").permitAll()
.antMatchers(HttpMethod.OPTIONS , "/*/**").permitAll()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
//handling authentication exceptions
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint);
//login configuration
http.formLogin()
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler);
http.formLogin()
.failureHandler(authenticationFailureHandler);
//logout configuration
http.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler);
//CORS configuration
http.addFilterBefore(corsFilter, ChannelProcessingFilter.class);
//CSRF configuration
http.csrf().requireCsrfProtectionMatcher(
new AndRequestMatcher(
// Apply CSRF protection to all paths that do NOT match the ones below
// We disable CSRF at login/logout, but only for OPTIONS methods to enable the browser preflight
new NegatedRequestMatcher(new AntPathRequestMatcher("/login*/**", HttpMethod.OPTIONS.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/logout*/**", HttpMethod.OPTIONS.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.GET.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.HEAD.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.OPTIONS.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/api*/**", HttpMethod.TRACE.toString()))
)
);
// CSRF tokens handling
http.addFilterAfter(new CSRFTokenResponseCookieBindingFilter(), CsrfFilter.class);
}
}
The problem is in the front side and the angular 4 configuration, the CSRF documentation is so poor and there is no full example of CSRF implementation in the Internet.
So below is my login service:
#Injectable()
export class LoginService {
private loginUrl = 'http://localhost:51030/login';
constructor(private http: Http) {}
preFlight() {
return this.http.options(this.loginUrl);
}
login(username: string , password: string) {
let headers = new Headers();
headers.append('Content-Type', 'application/x-www-form-urlencoded');
let options = new RequestOptions({headers: headers});
let body = "username="+username+"&password="+password;
return this.http.post(this.loginUrl , body , options);
}
}
And in the login component I execute the option request in the ngOnInit life cycle hook:
#Component({
templateUrl: './login-layout.component.html'
})
export class LoginLayoutComponent implements OnInit {
credentials = {username: '' , password: ''};
constructor(private loginService: LoginService){}
ngOnInit() {
this.loginService.preFlight()
.subscribe();
}
login() {
this.loginService.login(this.credentials.username , this.credentials.password)
.subscribe(
response=>{
console.log(response) ;
},error=>{
console.log(error);
}
);
}
}
The preflight goes well and I get the 200 OK status on the options request plus a temporary JSEEIONID and the XSRF-TOKEN Cookie.
So in my app module I added this as said in the angular docs:
{
provide: XSRFStrategy,
useValue: new CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN')
},
BUT, when I try to execute a POST request with the credentials or any request to the back I got 403 Forbidden: "Could not verify the provided CSRF token because your session was not found."
So Please how can I solve this, can any one point me to right direction cause I have no clue on how to make this work!!
And Thanks!!!

To solve the csrf problem between spring security and angular, you have to do that.
In SecurityConfiguration (WebSecurityConfig),replace http.csrf().disable(); by
http.csrf()
.ignoringAntMatchers ("/login","/logout")
.csrfTokenRepository (this.getCsrfTokenRepository());
}
private CsrfTokenRepository getCsrfTokenRepository() {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
tokenRepository.setCookiePath("/");
return tokenRepository;
{
the default angular csrf interceptor does not always work.So you have to implement your own interceptor.
import {Injectable, Inject} from '#angular/core';
import {HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler,
HttpEvent} from '#angular/common/http';
import {Observable} from "rxjs";
#Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {
constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let requestMethod: string = req.method;
requestMethod = requestMethod.toLowerCase();
if (requestMethod && (requestMethod === 'post' || requestMethod === 'delete' || requestMethod === 'put')) {
const headerName = 'X-XSRF-TOKEN';
let token = this.tokenExtractor.getToken() as string;
if (token !== null && !req.headers.has(headerName)) {
req = req.clone({headers: req.headers.set(headerName, token)});
}
}
return next.handle(req);
}
}
And finally add it in your providers (app.module.ts)
providers: [{ provide: HTTP_INTERCEPTORS, useClass: HttpXsrfInterceptor, multi: true }]
Think about putting in your imports.
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-CSRF-TOKEN'
}),

I am surprised that you are doing so much work for CSRF and CORS as Spring Security and Angular have support built in. Spring Security has CSRF enabled by default.
The spring security manual has good documentation about configuring csrf:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#csrf
And googling for "Angular 2 Spring Security csrf" gives several examples (and also how I found your post). Here is one:
https://medium.com/spektrakel-blog/angular2-and-spring-a-friend-in-security-need-is-a-friend-against-csrf-indeed-9f83eaa9ca2e

Related

SSO between App and webview inside the app

My user signs into my app using Amazon Cognito using this plugin.
I also have a spring boot application ui, secured by cognito as well.
At some point in my app flow, i want to show a webview of the spring boot application to let the user configure additional stuff.
How do i do it without having the user sign in again?
Would it be bad practice if i created an endpoint called /login/{username}/{password} that uses the SecurityContextHolder to sign the user in and redirect to /home?
I finally got it working.
First i logged in, and made my code stop somewhere using the debugger, so i could look up the SecurityContextHolder.getContext().getAuthentication(). My Authentication object is of type OAuth2AuthenticationToken. I took a close look at it, and decided to replicate it.
I did so inside a custom AuthenticationManager, and returned my OAuth2AuthenticationToken in the overriden authenticate method.
CustomAuthenticationManager.java
#Component
public class CustomAuthenticationManager implements AuthenticationManager {
#Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String token = ((Jwt)authentication.getPrincipal()).getTokenValue();
if (token == null)
throw new BadCredentialsException("Invalid token");
return convertAccessToken(token);
}
public OAuth2AuthenticationToken convertAccessToken(String accessToken){
Jwt decode = Tools.parseToken(accessToken);
List<GrantedAuthority> authorities = new ArrayList<>();
for (String s : ((String[]) decode.getClaims().get("cognito:groups"))) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + s));
}
Map<String, Object> claims = decode.getClaims();
OidcIdToken oidcIdToken = new OidcIdToken(decode.getTokenValue(), decode.getIssuedAt(), decode.getExpiresAt(), claims);
DefaultOidcUser user = new DefaultOidcUser(authorities, oidcIdToken, "email");
return new OAuth2AuthenticationToken(user, authorities, "cognito");
}
}
Also i put this in a static Tools.java
public static Jwt parseToken(String accessToken) {
DecodedJWT decode = com.auth0.jwt.JWT.decode(accessToken);
HashMap<String, Object> headers = new HashMap<>();
headers.put("alg", decode.getHeaderClaim("alg").asString());
headers.put("kid", decode.getHeaderClaim("kid").asString());
HashMap<String, Object> claims = new HashMap<>();
decode.getClaims().forEach((k, v) -> {
switch(k){
case "cognito:roles":
case "cognito:groups":
claims.put(k, v.asArray(String.class));
break;
case "auth_time":
case "exp":
case "iat":
claims.put(k, v.asLong());
break;
default:
claims.put(k, v.asString());
break;
}
});
return new Jwt(accessToken, decode.getIssuedAt().toInstant(), decode.getExpiresAt().toInstant(), headers, claims);
}
Then i created two endpoints. One that is my "login page", and one that my filter goes to. So in my login page i take in an access token, store it in the sesion, then redirect to my other endpoint that pasess through the filter.
TokenLoginController.java
#Component
#RestController
public class TokenLoginController {
#GetMapping(value="/login/token/{token}")
#PermitAll
public void setSession(#PathVariable("token") String token, HttpSession session, HttpServletResponse response) throws IOException {
session.setAttribute("access_token", token);
response.sendRedirect("/login/token");
}
#GetMapping(value="/login/token")
#PermitAll
public void setSession() {
}
}
The filter extends AbstractAuthenticationProcessingFilter and looks up the access token from the session, creates the OAuth2AuthenticationToken, and authenticates with it.
StickyAuthenticationFilter.java
public class StickyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public StickyAuthenticationFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authenticationManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws AuthenticationException, IOException, ServletException {
String access_token = (String)servletRequest.getSession().getAttribute("access_token");
if (access_token != null) {
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(Tools.parseToken(access_token));
return getAuthenticationManager().authenticate(authRequest);
}
throw new RuntimeException("Invalid access token");
}
}
And finally, my SecurityConfig ties it all together like this:
#EnableWebSecurity
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends VaadinWebSecurity {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/login/token/*", "/login/token").permitAll().and()
.addFilterBefore(new StickyAuthenticationFilter("/login/token", new CustomAuthenticationManager()), BearerTokenAuthenticationFilter.class)
.oauth2ResourceServer(oauth2 -> oauth2.jwt())
.authorizeRequests()
.antMatchers("/user/**")
.authenticated();
super.configure(http);
setOAuth2LoginPage(http, "/oauth2/authorization/cognito");
http.oauth2Login(l -> l.userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper()));
}
#Override
public void configure(WebSecurity web) throws Exception {
// Customize your WebSecurity configuration.
super.configure(web);
}
#Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
Optional<OidcUserAuthority> awsAuthority = (Optional<OidcUserAuthority>) authorities.stream()
.filter(grantedAuthority -> "ROLE_USER".equals(grantedAuthority.getAuthority()))
.findFirst();
if (awsAuthority.isPresent()) {
if (awsAuthority.get().getAttributes().get("cognito:groups") != null) {
mappedAuthorities = ((JSONArray) awsAuthority.get().getAttributes().get("cognito:groups")).stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
}
return mappedAuthorities;
};
}
}

Spring Cloud Gateway altering form data does not work

I defined this GatewayFilter:
EDIT More context information:
What I would like to achieve is to avoid the client providing its credentials to get an access token from an authorization server.
The client sends a POST request with user's credentials (username/password) and the gateway adds all complementary information like scope, client_id, grant_type etc... before forwarding the request to the authorization server.
#Component
public class OAuth2CredentialsAppenderGatewayFilterFactory extends AbstractGatewayFilterFactory<OAuth2CredentialsAppenderGatewayFilterFactory.Config> {
public OAuth2CredentialsAppenderGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
if ("x-www-form-urlencoded".equals(request.getHeaders().getContentType().getSubtype())) {
//This code is not executed, the call of formData.put does not do anything, even a breakpoint is not reached!
if (request.getMethod().equals(HttpMethod.POST)) {
exchange.getFormData().map(formData -> {
formData.put("key1", List.of("value1"));
formData.put("key2", List.of("value2"));
formData.put("key3", List.of("value3"));
return formData;
});
}
//This part of code works well, the header is added to the forwarded request
requestBuilder.header(HttpHeaders.AUTHORIZATION,
"Basic " + Base64Utils.encodeToString((this.uiClientId + ":" + this.uiClientSecret).getBytes()));
}
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
};
}
}
I use the filter like this:
- id: keycloak_token_route
uri: http://localhost:8180
predicates:
- Path=/kc/token
filters:
- OAuth2CredentialsAppender
- SetPath=/auth/realms/main/protocol/openid-connect/token
- name: RequestRateLimiter
args:
key-resolver: "#{#userIpKeyResolver}"
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 30
denyEmptyKey: false
The filter is well invoked but altering the incoming request body does not work.
I am new to the reactive world so I am a bit confused, any help will be appreciated.
For those who would like to do the same thing, this is how I solved my problem. Again I am not an expert of Reactive programming, I am still learning it so it might be a better answer.
#Component
public class OAuth2CredentialsAppenderGatewayFilterFactory extends AbstractGatewayFilterFactory<OAuth2CredentialsAppenderGatewayFilterFactory.Config> {
#Value("${uiservice.clientId}")
private String uiClientId;
#Value("${uiservice.clientSecret}")
private String uiClientSecret;
public OAuth2CredentialsAppenderGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (ServerWebExchange exchange, GatewayFilterChain chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
if (nonNull(request.getHeaders().getContentType()) && request.getHeaders().getContentType().equals(MediaType.APPLICATION_FORM_URLENCODED)) {
if (requireNonNull(request.getMethod()).equals(HttpMethod.POST)) {
//Use this filter to modify the request body
ModifyRequestBodyGatewayFilterFactory.Config requestConf = new ModifyRequestBodyGatewayFilterFactory.Config()
.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.setRewriteFunction(String.class, String.class, this.completeRequestBody());
requestBuilder.header(HttpHeaders.AUTHORIZATION, base64Encoding(this.uiClientId, this.uiClientSecret));
return new ModifyRequestBodyGatewayFilterFactory().apply(requestConf).filter(exchange.mutate().request(requestBuilder.build()).build(), chain);
}
}
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
};
}
/** Add some config params if needed */
public static class Config {
}
/** Complete request by adding required information to get the access token. Here we can get 2 type of token: client_credentials or password. If the param client_only=true we should get a client_credentials token */
private RewriteFunction<String, String> completeRequestBody() {
return (ServerWebExchange ex, String requestBody) -> {
requireNonNull(requestBody, "Body is required");
//if body contains only this, we should get a client_credentials token
var idForClientCredentialsOnly = "client=ui&client_only=true";
String finalRequestBody;
var joiner = new StringJoiner("");
if (idForClientCredentialsOnly.equalsIgnoreCase(requestBody)) {
joiner.add("grant_type=").add("client_credentials");
}
else {
joiner.add(requestBody);
if (!containsIgnoreCase(requestBody, "grant_type")) {
joiner.add("&grant_type=").add("password");
}
}
if (!containsIgnoreCase(requestBody, "scope")) {
joiner.add("&scope=").add("uiclient");//I use Keycloak so I specify the scope to get some extra information
}
finalRequestBody = joiner.toString();
return Mono.just(isBlank(finalRequestBody) ? requestBody : finalRequestBody);
};
}
}

Spring Boot Rest API #CrossOrigin Not working

I know this question asked already but I have the same issue and did not find any solution
Spring Boot, I have Rest API and added a cross-origin annotation
#CrossOrigin(origins = "*", allowedHeaders = "*")
But it still showing me an error
Access to XMLHttpRequest at 'http://localhost:8080/API/findUser' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
ERROR HttpErrorResponse {headers: HttpHeaders, status: 0, statusText: "Unknown Error", url: null, ok: false}
I also tried
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST", "PUT", "DELETE").allowedOrigins("*")
.allowedHeaders("*");
}
};
}
But not working
Just in case if somebody found this question after some Googleing, this could potentially solve the issue.
If you are using Spring security. You should enable cors() there as well. Set your WebSecurityConfigurerAdapter like this:
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.cors() //<-- Enables CORS
.and()
.antMatcher("/api/**")
.authorizeRequests()
.anyRequest()
.hasRole("admin")
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter());
}
}

How do I extract information from an incoming JWT that was generated by an external service?

How do I extract information from an incoming JWT that was generated by an external service? (Okta)
I need to perform a database lookup of user information based on one of the fields in the JWT. (I also want method-level security based on the scope of the JWT.)
The secret seems to be in using an AccessTokenConverter to extractAuthentication() and then use that to lookup UserDetails. I am stuck because every example I can find includes setting up an Authorization Server, which I don't have, and I can't tell if the JwtAccessTokenConverter will work on the Resource Server.
My resource server runs and handles requests, but my custom JwtAccessTokenConverter is never getting called during incoming requests;
All of my requests are coming in with a principal of anonymousUser.
I am using Spring 5.1.1.
My Resource Server Configuration
#Configuration
#EnableResourceServer
public class OauthResourceConfig extends ResourceServerConfigurerAdapter {
#Value("${oauth2.audience}")
String audience;
#Value("${oauth2.baseUrl}/v1/keys")
String jwksUrl;
#Override
public void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/api/**").permitAll();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.tokenServices(tokenServices())
.resourceId(audience);
}
#Primary
#Bean
public DefaultTokenServices tokenServices() throws Exception {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
return tokenServices;
}
#Bean
public TokenStore tokenStore() {
return new JwkTokenStore(jwksUrl, accessTokenConverter());
}
#Bean
public AccessTokenConverter accessTokenConverter() {
return new CustomJwtAccessTokenConverter();
}
}
My Custom Access Token Converter
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
LinkedHashMap userDetails = (LinkedHashMap) map.get("userDetails");
if (userDetails != null) {
... Do the database lookup here ...
Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();
userAuthentication = new UsernamePasswordAuthenticationToken(extendedPrincipal,
userAuthentication.getCredentials(), authorities);
}
}
return new OAuth2Authentication(authentication.getOAuth2Request(), userAuthentication);
}
}
And my Resource
#GET
#PreAuthorize("#oauth2.hasScope('openid')")
public Response getRecallsByVin(#QueryParam("vin") String vin,
#QueryParam("page") Integer pageNumber,
#QueryParam("pageSize") Integer pageSize) {
List<VehicleNhtsaCampaign> nhtsaCampaignList;
List<OpenRecallsDto> nhtsaCampaignDtoList;
SecurityContext securityContext = SecurityContextHolder.getContext();
Object principal = securityContext.getAuthentication().getPrincipal();
... More irrelevant code follows ...
First of all, the #PreAuthorize annotation isn't doing anything. If I change it to #PreAuthorize("#oauth2.hasScope('FooBar')") it still lets the request in.
Secondly, I need to grab other information off the JWT so I can do a user lookup in my database. I thought that by adding the accessTokenConverter() in the resource server config, the JWT would be parsed and placed into the securityContext.getAuthentication() response. Instead all I'm getting is "anonymousUser".
UPDATE: I later found out the data I need is coming in a custom header, so I don't need to extract anything from the JWT. I was never able to validate any of the suggested answers.
Are you using Spring Boot?
The Spring Security 5.1 has support for JWT access tokens. For example, you could just supply a new JwtDecoder:
https://github.com/okta/okta-spring-boot/blob/spring-boot-2.1/oauth2/src/main/java/com/okta/spring/boot/oauth/OktaOAuth2ResourceServerAutoConfig.java#L62-L84
You can create a filter that validates and sets token to SecurityContextHolder. This is what I have done in my project using jsonwebtoken dependency:
public class JWTFilter extends GenericFilterBean {
private String secretKey = 'yoursecret';
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = resolveToken(httpServletRequest);
if (validateToken(jwt)) {
Authentication authentication = getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private String resolveToken(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
public Authentication getAuthentication(String 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);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
} catch (MalformedJwtException e) {
} catch (ExpiredJwtException e) {
} catch (UnsupportedJwtException e) {
} catch (IllegalArgumentException e) {
}
return false;
}
}
You can then access your token from SecurityContextHolder.
For cleaner way to access token fields, I have created POJO models of my token from http://www.jsonschema2pojo.org/

Why HandshakeRequest doesn't return HttpSession in my ServerEndpointConfig.Configurator?

There is a good example for sharing HttpSession between Websocket and Rest service. (Spring DispatchServlet cannot find resource within Jetty) But it doesn't work for me. I'm not sure is there any thing I'm missing?
I'm using Jetty as websocket server and also I created a WebApp as well which injected by SpringConfig.
private void init() throws Exception
{
Server server = new Server();
// Create SSL Connector
ServerConnector serverConnector = getSSLConnector(server);
// Bundle to server
server.setConnectors(new Connector[] { serverConnector });
// Create request handler collection
HandlerCollection handlers = new HandlerCollection();
// Add WebSocket handler
final ServletContextHandler servletContextHandler = getWebSocketContextHandler();
handlers.addHandler(servletContextHandler);
// Add Servlet handler
handlers.addHandler(getWebAppServletContextHandler());
server.setHandler(handlers);
// Initial WebSocket
WebSocketServerContainerInitializer.configureContext(servletContextHandler);
// Start Jetty
server.start();
server.join();
}
Both WebSocket and Rest are working under same port perfectly, of course, with different context paths.
Now, I created a Rest service:
#RequestMapping(value = "/login", method = RequestMethod.POST)
#Consumes({ MediaType.APPLICATION_JSON_VALUE })
#Produces({ MediaType.APPLICATION_JSON_VALUE })
public #ResponseBody Message login(#RequestBody Credential credential, #Context HttpServletRequest servlerRequest)
{
...
HttpSession session = servlerRequest.getSession(true);
session.setAttribute("userName", credential.getUserName());
...
Message message = new Message();
...
return message;
}
In this service I created a HttpSession and stored something in. As I said, it works, and so does the session.
Rest client:
public void login() throws KeyManagementException, NoSuchAlgorithmException
{
final String loginServiceUri = HTTP_SERVICE_BASE_URI + "/login";
ClientConfig clientConfig = new DefaultClientConfig();
...
Client client = Client.create(clientConfig);
WebResource webResource = client.resource(loginServiceUri);
ClientResponse response = webResource
.type("application/json")
.post(ClientResponse.class, new Credential("user","pass"));
if (response.getStatus() != 200) {
throw new RuntimeException("Failed : HTTP error code : " + response.getStatus());
}
List<NewCookie>cookies = response.getCookies();
ClientEndpointConfigurator.setCookies(cookies); <== Store cookies as well as session to ClientEndpointConfigrator class
Message message = response.getEntity(Message.class);
...
}
ClientEndpointConfigrator class has a static list for all cookies which like this:
public class ClientEndpointConfigurator extends ClientEndpointConfig.Configurator {
private static List<NewCookie> cookies = null;
public static void setCookies(List<NewCookie> cookies) {
ClientEndpointConfigurator.cookies = cookies;
}
...
#Override
public void beforeRequest(Map<String, List<String>> headers) {
...
if(null != cookies)
{
List<String> cookieList = new ArrayList<String>();
for(NewCookie cookie: cookies)
{
cookieList.add(cookie.toString());
}
headers.put("Cookie", cookieList);
}
...
}
}
beforeRequest() method will put all cookies to request header. If you inspect the cookieList, you will see:
[JSESSIONID=tvum36z6j2bc1p9uf2gumxguh;Version=1;Path=/rs;Secure]
Things looks prefect.
Finally, create a server end ServerEndpointConfigurator class, and override the modifyHandshake() method to retrieve the session and cookies
public class SpringServerEndpointConfigurator extends ServerEndpointConfig.Configurator {
#Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
super.modifyHandshake(sec, request, response);
httpSession = (HttpSession)request.getHttpSession(); <== **It returns null here!**
...
}
}
}
I can't get my HttpSession back! and if you print headers out, you will see the cookie has been changed:
Cookie: JSESSIONID="tvum36z6j2bc1p9uf2gumxguh";$Path="/rs"
Any one knows what's the reason?
All right, I figured it out, it's because I put WebSocket and Rest to different context handler. Jetty keeps handlers isolate to each other. To share session information, you have to put them together.
But if someone does want to separate them, it is still possible done by sharing SessionManager or SessionHandler. There are many ways to achieve this, you can inject SessionHandler to each ServletContext or just define it as a static variable and put it on somewhere every one can reach, each way works.