Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
I can test my JwtTokenAuthenticationFilter class. How can I write the test cases of this class using Mockito & JUnit? I can Only test this class.
I don't understand how I can mock the class.
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtConfig jwtConfig;
public JwtTokenAuthenticationFilter(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
System.out.println("Code is reachable");
// 1. get the authentication header. Tokens are supposed to be passed in the authentication header
String header = request.getHeader(jwtConfig.getHeader());
// 2. validate the header and check the prefix
if (header == null || !header.startsWith(jwtConfig.getPrefix())) {
chain.doFilter(request, response);
return;// If not valid, go to the next filter.
}
// If there is no token provided and hence the user won't be authenticated.
// It's Ok. Maybe the user accessing a public path or asking for a token.
// All secured paths that needs a token are already defined and secured in config class.
// And If user tried to access without access token, then he won't be authenticated and an exception will be thrown.
// 3. Get the token
String token = header.replace("Bearer","");
try { // exceptions might be thrown in creating the claims if for example the token is expired
// 4. Validate the token
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret().getBytes())
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
if (username != null) {
#SuppressWarnings("unchecked")
List<String> authorities = (List<String>) claims.get(ApplicationConstant.tokenAuthorities);
List<GrantedAuthority> grantAuthorities = new ArrayList<GrantedAuthority>();
// 5. Create auth object
// UsernamePasswordAuthenticationToken:A built-in object, used by spring to represent the current authenticated / being authenticated user.
// It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
for (String authName : authorities) {
grantAuthorities.add(new SimpleGrantedAuthority(authName));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username, null, grantAuthorities);
// 6. Authenticate the user
// Now, user is authenticated
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
// In case of failure. Make sure it's clear; so guarantee user won't be authenticated
SecurityContextHolder.clearContext();
}
// go to the next filter in the filter chain
chain.doFilter(request, response);
}
}
Spring gives you some mocks:
MockHttpServletRequest: mocked implementation of HttpServletRequest
MockHttpServletResponse: mocked implementation of HttpServletResponse
MockFilterChain: mocked implementation of FilterChain
MockFilterConfig: mocked implementation if FilterConfig
See the org.springframework.mock.web package for other mocks.
And here's some code that can help you to get started:
#RunWith(SpringRunner.class)
public class JwtTokenAuthenticationFilterTest {
#Before
public void before() {
SecurityContextHolder.clearContext();
}
#After
public void after() {
SecurityContextHolder.clearContext();
}
#Test
#SneakyThrows
public void doFilterInternal_shouldPopulateSecurityContext_whenTokenIsValid() {
String token = issueTokenForUser("john.doe");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = new MockFilterChain();
FilterConfig filterConfig = new MockFilterConfig();
JwtTokenAuthenticationFilter filter = new JwtTokenAuthenticationFilter();
filter.init(filterConfig);
filter.doFilter(request, response, filterChain);
filter.destroy();
assertThat(SecurityContextHolder.getContext().getAuthentication())
.satisfies(authentication -> {
assertThat(authentication).isNotNull();
assertThat(authentication.getName()).isEqualTo("john.doe");
});
}
private String issueTokenForUser(String username) {
return "xxxxx.yyyyy.zzzzz"; // Implement as per your needs
}
}
The above code uses AssertJ for assertions.
Related
Link for Authorization server: https://github.com/spring-projects/spring-authorization-server
This project pretty much has everything in terms of OAuth and Identity provider.
My question is, How to achieve multi-tenancy at the Identity provider level.
I know there are multiple ways to achieve multi-tenancy in general.
The scenario I am interested in is this:
An organization provides services to multiple tenants.
Each tenant is associated with a separate database (Data isolation including user data)
When a user visits dedicated Front-end app(per tenant) and negotiate access tokens from Identity provider
Identity provider then identifies tenant (Based on header/ Domain name) and generates access token with tenant_id
This access token then is passed on to down-stream services, which intern can extract tenant_id and decide the data source
I have a general idea about all the above steps, but I am not sure about point 4.
I am not sure How to configure different data sources for different tenants on the Identity Provider? How to add tenant_id in Token?
Link to the issue: https://github.com/spring-projects/spring-authorization-server/issues/663#issue-1182431313
This is not related to Spring auth Server, but related to approaches that we can think for point # 4
I remember the last time we implemented a similar approach, where we had below options
To have unique email addresses for the users thereby using the global database to authenticate the users and post authentication, set up the tenant context.
In case of users operating in more than 1 tenant, post authentication, we can show the list of tenant's that the user has access to, which enables setting the tenant context and then proceeding with the application usage.
More details can be read from here
This is really a good question and I really want to know how to do it in new Authorization Server in a proper way. In Spring Resource Server there is a section about Multitenancy. I did it successfully.
As far as new Spring Authorization Server multitenancy concerns. I have also done it for the password and the Client Credentials grant type.
But please note that although it is working but how perfect is this. I don't know because I just did it for learning purpose. It's just a sample. I will also post it on my github when I would do it for the authorization code grant type.
I am assuming that the master and tenant database configuration has been done. I can not provide the whole code here because it's lot of code. I will just provide the relevant snippets. But here is just the sample
#Configuration
#Import({MasterDatabaseConfiguration.class, TenantDatabaseConfiguration.class})
public class DatabaseConfiguration {
}
I used the separate database. What I did I used something like the following in the AuthorizationServerConfiguration.
#Import({OAuth2RegisteredClientConfiguration.class})
public class AuthorizationServerConfiguration {
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
....
http.addFilterBefore(new TenantFilter(), OAuth2AuthorizationRequestRedirectFilter.class);
SecurityFilterChain securityFilterChain = http.formLogin(Customizer.withDefaults()).build();
addCustomOAuth2ResourceOwnerPasswordAuthenticationProvider(http);
return securityFilterChain;
}
}
Here is my TenantFilter code
public class TenantFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LogManager.getLogger();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestUrl = request.getRequestURL().toString();
if (!requestUrl.endsWith("/oauth2/jwks")) {
String tenantDatabaseName = request.getParameter("tenantDatabaseName");
if(StringUtils.hasText(tenantDatabaseName)) {
LOGGER.info("tenantDatabaseName request parameter is found");
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
} else {
LOGGER.info("No tenantDatabaseName request parameter is found");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{'error': 'No tenant request parameter supplied'}");
response.getWriter().flush();
return;
}
}
filterChain.doFilter(request, response);
}
public static String getFullURL(HttpServletRequest request) {
StringBuilder requestURL = new StringBuilder(request.getRequestURL().toString());
String queryString = request.getQueryString();
if (queryString == null) {
return requestURL.toString();
} else {
return requestURL.append('?').append(queryString).toString();
}
}
}
Here is the TenantDBContextHolder class
public class TenantDBContextHolder {
private static final ThreadLocal<String> TENANT_DB_CONTEXT_HOLDER = new ThreadLocal<>();
public static void setCurrentDb(String dbType) {
TENANT_DB_CONTEXT_HOLDER.set(dbType);
}
public static String getCurrentDb() {
return TENANT_DB_CONTEXT_HOLDER.get();
}
public static void clear() {
TENANT_DB_CONTEXT_HOLDER.remove();
}
}
Now as there is already configuration for master and tenant database. In these configurations we also check for the TenantDBContextHolder
class that it contains the value or not. Because when request comes for token then we check the request and set it in TenantDBContextHolder. So base on this thread local variable right database is connected and the token issue to the right database. Then in the token customizer. You can use something like the following
public class UsernamePasswordAuthenticationTokenJwtCustomizerHandler extends AbstractJwtCustomizerHandler {
....
#Override
protected void customizeJwt(JwtEncodingContext jwtEncodingContext) {
....
String tenantDatabaseName = TenantDBContextHolder.getCurrentDb();
if (StringUtils.hasText(tenantDatabaseName)) {
URL issuerURL = jwtClaimSetBuilder.build().getIssuer();
String issuer = issuerURL + "/" + tenantDatabaseName;
jwtClaimSetBuilder.claim(JwtClaimNames.ISS, issuer);
}
jwtClaimSetBuilder.claims(claims ->
userAttributes.entrySet().stream()
.forEach(entry -> claims.put(entry.getKey(), entry.getValue()))
);
}
}
Now I am assuming that the Resource Server is also configure for multitenancy. Here is the link Spring Security Resource Server Multitenancy. Basically You have to configure two beans for multitenancy like the following
public class OAuth2ResourceServerConfiguration {
....
#Bean
public JWTProcessor<SecurityContext> jwtProcessor(JWTClaimsSetAwareJWSKeySelector<SecurityContext> keySelector) {
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
return jwtProcessor;
}
#Bean
public JwtDecoder jwtDecoder(JWTProcessor<SecurityContext> jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefault(), jwtValidator);
decoder.setJwtValidator(validator);
return decoder;
}
}
Now two classes for spring. From which you can get the tenant Identifier from your token.
#Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private final TenantDataSourceRepository tenantDataSourceRepository;
private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
....
#Override
public OAuth2TokenValidatorResult validate(Jwt token) {
String issuerURL = toTenant(token);
JwtIssuerValidator jwtIssuerValidator = validators.computeIfAbsent(issuerURL, this::fromTenant);
OAuth2TokenValidatorResult oauth2TokenValidatorResult = jwtIssuerValidator.validate(token);
String tenantDatabaseName = JwtService.getTenantDatabaseName(token);
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
return oauth2TokenValidatorResult;
}
private String toTenant(Jwt jwt) {
return jwt.getIssuer().toString();
}
private JwtIssuerValidator fromTenant(String tenant) {
String issuerURL = tenant;
String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenantDatabaseName);
if (tenantDataSource == null) {
throw new IllegalArgumentException("unknown tenant");
}
JwtIssuerValidator jwtIssuerValidator = new JwtIssuerValidator(issuerURL);
return jwtIssuerValidator;
}
}
Similarly
#Component
public class TenantJWSKeySelector implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
....
#Override
public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext) throws KeySourceException {
String tenant = toTenantDatabaseName(jwtClaimsSet);
JWSKeySelector<SecurityContext> jwtKeySelector = selectors.computeIfAbsent(tenant, this::fromTenant);
List<? extends Key> jwsKeys = jwtKeySelector.selectJWSKeys(jwsHeader, securityContext);
return jwsKeys;
}
private String toTenantDatabaseName(JWTClaimsSet claimSet) {
String issuerURL = (String) claimSet.getClaim("iss");
String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
return tenantDatabaseName;
}
private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenant);
if (tenantDataSource == null) {
throw new IllegalArgumentException("unknown tenant");
}
JWSKeySelector<SecurityContext> jwtKeySelector = fromUri(jwkSetUri);
return jwtKeySelector;
}
private JWSKeySelector<SecurityContext> fromUri(String uri) {
try {
return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri));
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
}
Now what about authorization code grant type grant type flow. I get the tenant identifier in this case too. But when it redirects me to login page then I lost the tenant identifier because I think it creates a new request for the login page from the authorization code request. Anyways I am not sure about it because I have to look into the code of authorization code flow that what it is actually doing. So my tenant identifier is losing when it redirects me to login page.
But in case of password grant type and client credentials grant type there is no redirection so I get the tenant identifier in later stages and I can successfully use it to put into my token claims.
Then on the resource server I get the issuer url. Get the tenant identifier from the issuer url. Verify it. And it connects to the tenant database on resource server.
How I tested it. I used the spring client. You can customize the request for authorization code flow. Password and client credentials to include the custom parameters.
Thanks.
------------------ Solve the Authorization Code login problem for multitenancy -------------
I solved this issue too. Actually what I did in my security configuration. I used the following configuration
public class SecurityConfiguration {
.....
#Bean(name = "authenticationManager")
public AuthenticationManager authenticationManager(AuthenticationManagerBuilder builder) throws Exception {
AuthenticationManager authenticationManager = builder.getObject();
return authenticationManager;
}
#Bean
#DependsOn(value = {"authenticationManager"})
public TenantUsernamePasswordAuthenticationFilter tenantAuthenticationFilter(AuthenticationManagerBuilder builder) throws Exception {
TenantUsernamePasswordAuthenticationFilter filter = new TenantUsernamePasswordAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager(builder));
filter.setAuthenticationDetailsSource(new TenantWebAuthenticationDetailsSource());
//filter.setAuthenticationFailureHandler(failureHandler());
return filter;
}
#Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
FederatedIdentityConfigurer federatedIdentityConfigurer = new FederatedIdentityConfigurer().oauth2UserHandler(new UserRepositoryOAuth2UserHandler());
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
http.addFilterBefore(tenantAuthenticationFilter(authenticationManagerBuilder), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests(authorizeRequests -> authorizeRequests.requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll()
.antMatchers("/resources/**", "/static/**", "/webjars/**").permitAll()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
......
.apply(federatedIdentityConfigurer);
return http.build();
}
Actually the problem was in case of Authorization Code is that you first redirect to login page. After successfully login you see the consent page. But when you comes to consent page then you lost the tenant parameter.
The reason is the spring internal class OAuth2AuthorizationEndpointFilter intercepts the request for Authorization Code. It checks user is authenticated or not. If user is not authenticated then it shows the login page. After successfully login it checks if consent is required. And if required then it makes a redirect uri with just three parameters. Here is the spring internal code
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication) throws IOException {
....
if (hasConsentUri()) {
String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
.queryParam(OAuth2ParameterNames.SCOPE, String.join(" ", requestedScopes))
.queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
.queryParam(OAuth2ParameterNames.STATE, state)
.toUriString();
this.redirectStrategy.sendRedirect(request, response, redirectUri);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Displaying generated consent screen");
}
DefaultConsentPage.displayConsent(request, response, clientId, principal, requestedScopes, authorizedScopes, state);
}
}
See the above method is private and I found no way that I can customize it. May be there is but I didn't find it. Anyways now your consent controller is call. But there is no tenant Identifier. You can't get it. And after consent there is no way that it connects to tenant database base in identifier.
So the first step is to add tenant identifier to login page. And then after login you should have this tenant identifier so you can set it on your consent page. And after that when you submit your consent form then this parameter will be there.
Btw I did it some time ago and may be I miss something but this is what I did.
Now how you get your parameter at login page. I solved it using the following. First I created a constant as I have to access the name from multiple times
public interface Constant {
String TENANT_DATABASE_NAME = "tenantDatabaseName";
}
Create the following class
public class RedirectModel {
#NotBlank
private String tenantDatabaseName;
public void setTenantDatabaseName(String tenantDatabaseName) {
this.tenantDatabaseName = tenantDatabaseName;
}
public String getTenantDatabaseName() {
return tenantDatabaseName;
}
}
Then on my Login controller I get it using the following code
#Controller
public class LoginController {
#GetMapping("/login")
public String login(#Valid #ModelAttribute RedirectModel redirectModel, Model model, BindingResult result) {
if (!result.hasErrors()) {
String tenantDatabaseName = redirectModel.getTenantDatabaseName();
String currentDb = TenantDBContextHolder.getCurrentDb();
LOGGER.info("Current database is {}", currentDb);
LOGGER.info("Putting {} as tenant database name in model. So it can be set as a hidden form element ", tenantDatabaseName);
model.addAttribute(Constant.TENANT_DATABASE_NAME, tenantDatabaseName);
}
return "login";
}
}
So this is the first step that I have my tenant identifier in my login page that is send to me by request.
Now the configuration that I used in my Security configuration. You can see that I am using TenantUsernamePasswordAuthenticationFilter. Here is the filer
public class TenantUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger LOGGER = LogManager.getLogger();
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String tenantDatabaseName = obtainTenantDatabaseName(request);
LOGGER.info("tenantDatabaseName is {}", tenantDatabaseName);
LOGGER.info("Setting {} as tenant database name in thread local context.", tenantDatabaseName);
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
return super.attemptAuthentication(request, response);
}
private String obtainTenantDatabaseName(HttpServletRequest request) {
return request.getParameter(Constant.TENANT_DATABASE_NAME);
}
}
And in the configuration I am setting TenantWebAuthenticationDetailsSource on this filter which is here
public class TenantWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
#Override
public TenantWebAuthenicationDetails buildDetails(HttpServletRequest context) {
return new TenantWebAuthenicationDetails(context);
}
}
Here is the class
public class TenantWebAuthenicationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = 1L;
private String tenantDatabaseName;
public TenantWebAuthenicationDetails(HttpServletRequest request) {
super(request);
this.tenantDatabaseName = request.getParameter(Constant.TENANT_DATABASE_NAME);
}
public TenantWebAuthenicationDetails(String remoteAddress, String sessionId, String tenantDatabaseName) {
super(remoteAddress, sessionId);
this.tenantDatabaseName = tenantDatabaseName;
}
public String getTenantDatabaseName() {
return tenantDatabaseName;
}
}
Now after spring authenticates the user then I have the tenant name in details. Then in the consent controller I use
#Controller
public class AuthorizationConsentController {
....
#GetMapping(value = "/oauth2/consent")
public String consent(Authentication authentication, Principal principal, Model model,
#RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
#RequestParam(OAuth2ParameterNames.SCOPE) String scope,
#RequestParam(OAuth2ParameterNames.STATE) String state) {
......
String registeredClientName = registeredClient.getClientName();
Object webAuthenticationDetails = authentication.getDetails();
if (webAuthenticationDetails instanceof TenantWebAuthenicationDetails) {
TenantWebAuthenicationDetails tenantAuthenticationDetails = (TenantWebAuthenicationDetails)webAuthenticationDetails;
String tenantDatabaseName = tenantAuthenticationDetails.getTenantDatabaseName();
model.addAttribute(Constant.TENANT_DATABASE_NAME, tenantDatabaseName);
}
model.addAttribute("clientId", clientId);
.....
return "consent-customized";
}
}
Now I have my tenant identifier on my consent page. After submitting it it's in the request parameter.
There is another class that I used and it was
public class TenantLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public TenantLoginUrlAuthenticationEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
#Override
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
String tenantDatabaseNameParamValue = request.getParameter(Constant.TENANT_DATABASE_NAME);
String redirect = super.determineUrlToUseForThisRequest(request, response, exception);
String url = UriComponentsBuilder.fromPath(redirect).queryParam(Constant.TENANT_DATABASE_NAME, tenantDatabaseNameParamValue).toUriString();
return url;
}
}
Anyways this is how I solved it. I don't have any such requirement in any of my project but I want to do it using this new server so I just solved it in this way.
Anyways there is lot of code. I tested it using the Spring oauth2 client and it was working. Hopefully I will create some project and upload it on my Github. Once I will run it again then I will put more explanation here of the flow. Specially for the last part that after submitting the consent how it set in the Thread Local variable.
After that everything is straight forward.
Hopefully it will help.
Thanks
I am implementing the spring authorization server and I want to add few custom properties to the token response json. Below is how I want the response to be.
{
"access_token": *jwt*,
"scope": "articles.read openid",
"token_type": "Bearer",
"expires_in": 299,
***"customvalue1":99***
}
I have seen multiple posts in stack overflow where similar topic is discussed, but in those scenarios the additional data is added either to the claim or header of jwt. My requirement is to add it outside of the jwt.
I tried to implement OAuth2TokenCustomizer, but this allows only the claims or headers of the jwt to be modified. Can anyone pls help?
To anyone coming here looking for answer:
I ended up overriding OAuth2TokenEndpointFilter. It has a authentication successhandler which can be injected to perform any additional token response manipulation.
#Bean
public Customizer<OAuth2TokenEndpointConfigurer> customizeTokenEndpoint() {
return tokenEndpoint -> tokenEndpoint
.accessTokenResponseHandler(success());
}
#Bean(name = "token")
public AuthenticationSuccessHandler success() {
return new TokenResponseSuccessHandler();
}
Then inside success handler,
#Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
final OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
******
**
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
if(additionalParameters.size()==0)
additionalParameters=new HashMap<>();
additionalParameters.put("hi","hi");
Finally use, OAuth2AccessTokenResponse.Builder to build a new response.
In case you are using the new authorization server then creating this bean will help you achieve your goal. The good thing, once the bean is detected it will automatically be applied.
#Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
return context -> {
Authentication principal = context.getPrincipal();
//context.getTokenType().getValue().equals("access_token")
if (Objects.equals(context.getTokenType().getValue(), "access_token") && principal instanceof UsernamePasswordAuthenticationToken) {
Set<String> authorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
User user = (User) principal.getPrincipal();
context.getClaims().claim("authorities", authorities)
.claim("user", user);
}
};
}
Duplicate of How to create custom claims in JWT using spring-authorization-server
This class and the method maybe help you.You can find the class init place
I am trying to fetch a custom attribute (phone_number) in the form of a claim from Keycloak. I am following the steps given here. Below are attached screenprints of the steps I have executed.
Adding attribute to user
Protocol mapper
Protocol mapper is available in Client scope -> evaluate for the above-mentioned client
I am now trying to access this attribute in a filter as follows.
public class FilterTest extends OncePerRequestFilter {
public static final String PHONE_NUMBER = "phone_number";
public FilterTest() {
}
#Override
protected void doFilterInternal(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if( !AnonymousAuthenticationToken.class.isAssignableFrom(authentication.getClass()) ){
Principal principal = (Principal) authentication.getPrincipal();
if (principal instanceof KeycloakPrincipal) {
KeycloakPrincipal<KeycloakSecurityContext> kp = (KeycloakPrincipal<KeycloakSecurityContext>) principal;
AccessToken token = kp.getKeycloakSecurityContext().getToken();
Map<String, Object> otherClaims = token.getOtherClaims();
System.out.println("Phone number => "+otherClaims.get(PHONE_NUMBER); // null pointer
}
}
filterChain.doFilter(request, response);
}
}
Additional things that I have tried.
Clear the realm cache.
Add the built-in phone number protocol mapper.
The above two steps also didn't yield any results for me.
I am not sure what I am missing here. Any help is appreciated.
Keycloak is mapping some of the common fields from the custom attributes directly to the fields of IDToken class. In my case, phone number was a field in the IDToken class and I was trying to fetch it from otherClaims map. The following is the change in code snip that got me up and running.
AccessToken token = kp.getKeycloakSecurityContext().getToken();
phoneNumber = token.getPhoneNumber();
I am following a tutorial about JWT in a spring reactive context (webflux).
The token generation is working fine, however the authorization is not working when I use the Authorization with bearer
Here is what I have done:
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class WebSecurityConfig{
#Autowired private JWTReactiveAuthenticationManager authenticationManager;
#Autowired private SecurityContextRepository securityContext;
#Bean public SecurityWebFilterChain configure(ServerHttpSecurity http){
return http.exceptionHandling()
.authenticationEntryPoint((swe , e) -> {
return Mono.fromRunnable(()->{
System.out.println( "authenticationEntryPoint user trying to access unauthorized api end points : "+
swe.getRequest().getRemoteAddress()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(()->{
System.out.println( "accessDeniedHandler user trying to access unauthorized api end points : "+
swe.getPrincipal().block().getName()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
})
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContext)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/auth/login").permitAll()
.anyExchange().authenticated()
.and()
.build();
}
As you can see, I want to simply deny all not authorized requests other than login or options based ones.
The login is working fine and I'm getting a token.
But trying to logout (a tweak that I implemented my self to make it state-full since I m only learning) is not working.
Here is my logout controller:
#RestController
#RequestMapping(AuthController.AUTH)
public class AuthController {
static final String AUTH = "/auth";
#Autowired
private AuthenticationService authService;
#PostMapping("/login")
public Mono<ResponseEntity<?>> login(#RequestBody AuthRequestParam arp) {
String username = arp.getUsername();
String password = arp.getPassword();
return authService.authenticate(username, password);
}
#PostMapping("/logout")
public Mono<ResponseEntity<?>> logout(#RequestBody LogoutRequestParam lrp) {
String token = lrp.getToken();
return authService.logout(token);
}
}
The logout request is as below:
As stated in images above, I believe that I m doing fine, however I m getting the error log message:
authenticationEntryPoint user trying to access unauthorized api end points : /127.0.0.1:45776 in /auth/logout
Here is my security context content:
/**
* we use this class to handle the bearer token extraction
* and pass it to the JWTReactiveAuthentication manager so in the end
* we produce
*
* simply said we extract the authorization we authenticate and
* depending on our implementation we produce a security context
*/
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
#Autowired
private JWTReactiveAuthenticationManager authenticationManager;
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
String authorizationHeaderContent = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if( authorizationHeaderContent !=null && !authorizationHeaderContent.isEmpty() && authorizationHeaderContent.startsWith("Bearer ")){
String token = authorizationHeaderContent.substring(7);
Authentication authentication = new UsernamePasswordAuthenticationToken(token, token);
return this.authenticationManager.authenticate(authentication).map((auth) -> {
return new SecurityContextImpl(auth);
});
}
return Mono.empty();
}
#Override
public Mono<Void> save(ServerWebExchange arg0, SecurityContext arg1) {
throw new UnsupportedOperationException("Not supported yet.");
}
}
I'm unable to see or find any issue or error that I have made. Where is the mistake?
There's a difference in writing
//Wrong
Jwts.builder()
.setSubject(username)
.setClaims(claims)
and
//Correct
Jwts.builder()
.setClaims(claims)
.setSubject(username)
Indeed, look at setSubject method in the DefaultJwtBuilder class :
#Override
public JwtBuilder setSubject(String sub) {
if (Strings.hasText(sub)) {
ensureClaims().setSubject(sub);
} else {
if (this.claims != null) {
claims.setSubject(sub);
}
}
return this;
}
When setSubject(username) is called first, ensureClaims() creates a DefaultClaims without yours and if you call setClaims(claims) the precedent subject is lost ! This JWT builder is bogus.
Otherwise, you're importing the wrong Role class in JWTReactiveAuthenticationManager, you have to replace :
import org.springframework.context.support.BeanDefinitionDsl.Role;
by
import com.bridjitlearning.www.jwt.tutorial.domain.Role;
Last and not least, validateToken() will return always false because of the check(token). put call is coming too late, you have to be aware of that. Either you remove this check or you move the put execution before calling the check method.
I'am not sure about what you want to do with resignTokenMemory, so i'll let you fix it by your own:
public Boolean validateToken(String token) {
return !isTokenExpired(token) && resignTokenMemory.check(token);
}
Another thing, your token is valid only 28,8 second, for testing raison i recommend you to expiraiton * 1000.
I am new to JWT. There isn't much information available in the web, since I came here as a last resort. I already developed a spring boot application using spring security using spring session. Now instead of spring session we are moving to JWT. I found few links and now I can able to authenticate a user and generate token. Now the difficult part is, I want to create a filter which will be authenticate every request to the server,
How will the filter validate the token? (Just validating the signature is enough?)
If someone else stolen the token and make rest call, how will I verify that.
How will I by-pass the login request in the filter? Since it doesn't have authorization header.
Here is a filter that can do what you need :
public class JWTFilter extends GenericFilterBean {
private static final Logger LOGGER = LoggerFactory.getLogger(JWTFilter.class);
private final TokenProvider tokenProvider;
public JWTFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,
ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = this.resolveToken(httpServletRequest);
if (StringUtils.hasText(jwt)) {
if (this.tokenProvider.validateToken(jwt)) {
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(servletRequest, servletResponse);
this.resetAuthenticationAfterRequest();
} catch (ExpiredJwtException eje) {
LOGGER.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());
((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
LOGGER.debug("Exception " + eje.getMessage(), eje);
}
}
private void resetAuthenticationAfterRequest() {
SecurityContextHolder.getContext().setAuthentication(null);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(SecurityConfiguration.AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String jwt = bearerToken.substring(7, bearerToken.length());
return jwt;
}
return null;
}
}
And the inclusion of the filter in the filter chain :
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
public final static String AUTHORIZATION_HEADER = "Authorization";
#Autowired
private TokenProvider tokenProvider;
#Autowired
private AuthenticationProvider authenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.authenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
JWTFilter customFilter = new JWTFilter(this.tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
// #formatter:off
http.authorizeRequests().antMatchers("/css/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/authenticate").permitAll()
.anyRequest().fullyAuthenticated()
.and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
.and().logout().permitAll();
// #formatter:on
http.csrf().disable();
}
}
The TokenProvider class :
public class TokenProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
#Value("${spring.security.authentication.jwt.validity}")
private long tokenValidityInMilliSeconds;
#Value("${spring.security.authentication.jwt.secret}")
private String secretKey;
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.joining(","));
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime expirationDateTime = now.plus(this.tokenValidityInMilliSeconds, ChronoUnit.MILLIS);
Date issueDate = Date.from(now.toInstant());
Date expirationDate = Date.from(expirationDateTime.toInstant());
return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
.signWith(SignatureAlgorithm.HS512, this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody();
Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
LOGGER.info("Invalid JWT signature: " + e.getMessage());
LOGGER.debug("Exception " + e.getMessage(), e);
return false;
}
}
}
Now to answer your questions :
Done in this filter
Protect your HTTP request, use HTTPS
Just permit all on the /login URI (/authenticate in my code)
I will focus in the general tips on JWT, without regarding code implemementation (see other answers)
How will the filter validate the token? (Just validating the signature is enough?)
RFC7519 specifies how to validate a JWT (see 7.2. Validating a JWT), basically a syntactic validation and signature verification.
If JWT is being used in an authentication flow, we can look at the validation proposed by OpenID connect specification 3.1.3.4 ID Token Validation. Summarizing:
iss contains the issuer identifier (and aud contains client_id if using oauth)
current time between iat and exp
Validate the signature of the token using the secret key
sub identifies a valid user
If someone else stolen the token and make rest call, how will I verify that.
Possesion of a JWT is the proof of authentication. An attacker who stoles a token can impersonate the user. So keep tokens secure
Encrypt communication channel using TLS
Use a secure storage for your tokens. If using a web front-end consider to add extra security measures to protect localStorage/cookies against XSS or CSRF attacks
set short expiration time on authentication tokens and require credentials if token is expired
How will I by-pass the login request in the filter? Since it doesn't have authorization header.
The login form does not require a JWT token because you are going to validate the user credential. Keep the form out of the scope of the filter. Issue the JWT after successful authentication and apply the authentication filter to the rest of services
Then the filter should intercept all requests except the login form, and check:
if user authenticated? If not throw 401-Unauthorized
if user authorized to requested resource? If not throw 403-Forbidden
Access allowed. Put user data in the context of request( e.g. using a ThreadLocal)
Take a look at this project it is very good implemented and has the needed documentation.
1. It the above project this is the only thing you need to validate the token and it is enough. Where token is the value of the Bearer into the request header.
try {
final Claims claims = Jwts.parser().setSigningKey("secretkey")
.parseClaimsJws(token).getBody();
request.setAttribute("claims", claims);
}
catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
2. Stealing the token is not so easy but in my experience you can protect yourself by creating a Spring session manually for every successfull log in. Also mapping the session unique ID and the Bearer value(the token) into a Map (creating a Bean for example with API scope).
#Component
public class SessionMapBean {
private Map<String, String> jwtSessionMap;
private Map<String, Boolean> sessionsForInvalidation;
public SessionMapBean() {
this.jwtSessionMap = new HashMap<String, String>();
this.sessionsForInvalidation = new HashMap<String, Boolean>();
}
public Map<String, String> getJwtSessionMap() {
return jwtSessionMap;
}
public void setJwtSessionMap(Map<String, String> jwtSessionMap) {
this.jwtSessionMap = jwtSessionMap;
}
public Map<String, Boolean> getSessionsForInvalidation() {
return sessionsForInvalidation;
}
public void setSessionsForInvalidation(Map<String, Boolean> sessionsForInvalidation) {
this.sessionsForInvalidation = sessionsForInvalidation;
}
}
This SessionMapBean will be available for all sessions. Now on every request you will not only verify the token but also you will check if he mathces the session (checking the request session id does matches the one stored into the SessionMapBean). Of course session ID can be also stolen so you need to secure the communication. Most common ways of stealing the session ID is Session Sniffing (or the Men in the middle) and Cross-site script attack. I will not go in more details about them you can read how to protect yourself from that kind of attacks.
3. You can see it into the project I linked. Most simply the filter will validated all /api/* and you will login into a /user/login for example.