I have implemented JWT token authorization & authentication from Spring resource server dependency. Here is the config file:
#Configuration
#RequiredArgsConstructor
#EnableWebSecurity
public class WebSecurityConfig {
#Value("${app.chat.jwt.public.key}")
private RSAPublicKey publicKey;
#Value("${app.chat.jwt.private.key}")
private RSAPrivateKey privateKey;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.exceptionHandling(
exceptions ->
exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()));
http.authorizeHttpRequests()
.requestMatchers("/auth/sign-in").permitAll()
.requestMatchers("/auth/sign-up").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic(Customizer.withDefaults())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
#SneakyThrows
#Bean
public JwtEncoder jwtEncoder() {
var jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
#SneakyThrows
#Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
var jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
var jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
It works fine. I have AuthController where I have implemented endpoints for sign-in, sign-up, and refresh token. In each endpoint, I return a response with an access token and a refresh token. Here is the controller:
#RestController
#RequestMapping("/auth")
#RequiredArgsConstructor
public class AuthController {
private final JwtTokenService tokenService;
private final AuthenticationManager authManager;
private final UserDetailsService usrDetailsService;
private final UserService userService;
record LoginRequest(String username, String password) {}
#PostMapping("/sign-in")
public TokensResponse login(#RequestBody LoginRequest request) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(request.username, request.password);
authManager.authenticate(authenticationToken);
var user = (User) usrDetailsService.loadUserByUsername(request.username);
String accessToken = tokenService.generateAccessToken(user);
String refreshToken = tokenService.generateRefreshToken(user);
return new TokensResponse(accessToken, refreshToken);
}
record SignUpRequest(String username, String password){}
#PostMapping("/sign-up")
public TokensResponse signUp(#RequestBody SignUpRequest signUpRequest) {
User registeredUser = userService.register(new AuthRequestDto(signUpRequest.username(), signUpRequest.password()));
String accessToken = tokenService.generateAccessToken(registeredUser);
String refreshToken = tokenService.generateRefreshToken(registeredUser);
return new TokensResponse(accessToken, refreshToken);
}
#PreAuthorize("hasRole('REFRESH_TOKEN')")
#GetMapping("/token/refresh")
public TokensResponse refreshToken(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
String previousRefreshToken = headerAuth.substring(7);
String username = tokenService.parseToken(previousRefreshToken);
var user = (User) usrDetailsService.loadUserByUsername(username);
String accessToken = tokenService.generateAccessToken(user);
String refreshToken = tokenService.generateRefreshToken(user);
return new TokensResponse(accessToken, refreshToken);
}
record TokensResponse(String accessToken, String refreshToken) {}
}
And here is TokenService class where I generate those tokens:
#Service
#RequiredArgsConstructor
public class JwtTokenServiceImpl implements JwtTokenService {
private final JwtEncoder jwtEncoder;
#Override
public String generateAccessToken(User user) {
Instant now = Instant.now();
String scope = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(2, ChronoUnit.MINUTES))
.subject(user.getUsername())
.claim("scope", scope)
.build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
#Override
public String generateRefreshToken(User user) {
Instant now = Instant.now();
String scope = "ROLE_REFRESH_TOKEN";
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(10, ChronoUnit.MINUTES))
.subject(user.getUsername())
.claim("scope", scope)
.build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
#Override
public String parseToken(String token) {
try {
SignedJWT decodedJWT = SignedJWT.parse(token);
return decodedJWT.getJWTClaimsSet().getSubject();
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
What I want to do is to restrict the refresh token to be used only for the refresh endpoint. Because what's the point of having a short-term live access token if you can use a refresh token for all endpoints? I have tried to give the refresh token scope REFRESH_TOKEN and added #PreAuthorize("hasRole('REFRESH_TOKEN')") annotation for the refresh token endpoint. But it doesn't work(I can still send access token to refresh endpoint and get new tokens), because Spring doesn't look for claims from token. He just loads the user from the database by username from token and checks his roles from here.
Please suggest how can I make the refresh token restricted only to one endpoint. Also would be great to make him one-time use but seems that I would need to store tokens somewhere for that.
the refresh token is bound to the
client to which it was issued.
source https://www.rfc-editor.org/rfc/rfc6749#section-6
Refresh token affect to client scope (it means all end-points). Therefore your expected is not feasibility.
I run the following configuration to enable basic authentication for my (non-spring boot) project
#Configuration
#EnableOpenApi
#EnableWebMvc
public class SpringFoxConfig implements WebMvcConfigurer {
#Bean
public Docket api() {
return new Docket(DocumentationType.OAS_30)
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(PublicAPI.class))
.paths(PathSelectors.any())
.build()
.securityContexts(Arrays.asList(securityContext()))
.securitySchemes(Arrays.asList(securityScheme())));
}
private SecurityScheme securityScheme() {
return new HttpAuthenticationBuilder()
.name("basic")
.scheme("basic")
.build();
}
private SecurityContext securityContext() {
return SecurityContext
.builder()
.securityReferences(securityReferences())
.operationSelector(operationContext -> true)
.build();
}
private List<SecurityReference> securityReferences() {
return singletonList(new SecurityReference("Authorization", new AuthorizationScope[] {new AuthorizationScope("global", "global")}));
}
}
this allows me to authorize my requests
but when testing the call, the authorization header isn't built nor sent with the request:
curl -X GET "https://localhost:8443/foo/rest/ws/info/get-master/code/awd" -H "accept: application/json"
Security reference name was different from the one defined in the securityScheme
private SecurityScheme securityScheme() {
return new HttpAuthenticationBuilder()
.name("basic")
.scheme("basic")
.build();
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.any())
.build();
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = new AuthorizationScope("global", "accessEverything");
return singletonList(new SecurityReference("basic", authorizationScopes));
}
I wanna run a schedule method and in this method I need to use information about user is logged in. However, when I run getPrincipal() my code gets nullPointException
#Component
public class Import extends WebSecurityConfigurerAdapter {
#Autowired
private ActivityRepository activityRepository;
#Scheduled(fixedRate = 300000)
public void importActivities() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Principal principal = (Principal) authentication.getPrincipal();
List<Activity> lastActivity = activityRepository.findFirstByOrderByStartDateDesc();
int lastEpoch = 0;
if (lastActivity.isEmpty() == false) {
lastEpoch = (int) (lastActivity.get(0).getStartDate().getTime() / 1000);
}
System.out.println(lastEpoch);
final RestTemplate restTemplate = new RestTemplate();
final HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getAccessToken(principal));
final HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<List<Activity>> rateResponse = restTemplate.exchange(
"https://www.strava.com/api/v3/athlete/activities?after=" + lastEpoch, HttpMethod.GET, entity,
new ParameterizedTypeReference<List<Activity>>() {
});
List<Activity> activities = rateResponse.getBody();
activityRepository.saveAll(activities);
}
private String getAccessToken(final Principal principal) {
final OAuth2Authentication oauth2Auth = (OAuth2Authentication) principal;
final OAuth2AuthenticationDetails oauth2AuthDetails = (OAuth2AuthenticationDetails) oauth2Auth.getDetails();
return oauth2AuthDetails.getTokenValue();
}
}
Thanks
EDIT: I'm using Swagger UI 2.5.0 and attempting to configure it to use oauth authentication. From what I understand from looking at the petstore example and other pieces I've read if I include a security schema & context in my Docket it should automatically display the button - is this the case? If so what else am I missing here?
My API's appear fine in the swagger UI - it's just that the authorize button (and therefore any means of authorization) is missing
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("api")
.select()
.apis(RequestHandlerSelectors.basePackage("com.example"))
.paths(PathSelectors.regex("/api.*"))
.build()
.securitySchemes(newArrayList(securitySchema()))
.securityContexts(newArrayList(securityContext()));
}
public static final String securitySchemaOAuth2 = "oauth2schema";
public static final String authorizationScopeGlobal = "global";
public static final String authorizationScopeGlobalDesc ="accessEverything";
private OAuth securitySchema() {
AuthorizationScope authorizationScope = new AuthorizationScope(authorizationScopeGlobal, authorizationScopeGlobal);
LoginEndpoint loginEndpoint = new LoginEndpoint("http://localhost:9999/sso/login");
GrantType grantType = new ImplicitGrant(loginEndpoint, "access_token");
return new OAuth(securitySchemaOAuth2, newArrayList(authorizationScope), newArrayList(grantType));
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("/api.*"))
.build();
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope
= new AuthorizationScope(authorizationScopeGlobal, authorizationScopeGlobalDesc);
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return newArrayList(
new SecurityReference(securitySchemaOAuth2, authorizationScopes));
}
I have very strange problem. In simple project I used Spring-Boot with oAuth2 (it is exactly jhipster generated project).
In services I connect with remote controllers (remote API) by restTemplate class. And I created special class to store cookieSession access to this remote API (this class has Session scope).
During authorization I save cookieSession from remote API to Session Scope class, and then when I make request to other part of remote API I use this seesionCookie.
Problem is, when I make asynchronous requesting from AngulrJS then sometimes Session scope class exist and sometimes it doesn't have data (is empty), but when I refresh website I have this data (without making next authorization). Whan I make synchronous requests there is no problem.
#Service
#Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AuthorizationOsipDataService implements Serializable {
private String cookieSession;
public String getCookieSession() {
return cookieSession;
}
public void setCookieSession(String cookieSession) {
this.cookieSession = cookieSession;
}
}
Service:
#Service
public class OsipService {
#Autowired
private RestTemplate restTemplate;
#Autowired
private AuthorizationOsipDataService authorizationOsipDataService;
public String signInToOsipAndGetCookieSession (String login, String password) throws SignInToOsipException {
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("j_username", login);
map.add("j_password", password);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(map, new HttpHeaders());
log.debug("Logging... user: '{}'", login);
ResponseEntity response = restTemplate.exchange(osipUrl + authorizationUrl, HttpMethod.POST, requestEntity, String.class);
if(isLogged(response)){
String cookieSession = response.getHeaders().getFirst(HttpHeaders.SET_COOKIE);
log.debug("Succes login, setting authorizationOsipDataService");
authorizationOsipDataService.setPassword(password);
authorizationOsipDataService.setUsername(login);
authorizationOsipDataService.setCookieSession(cookieSession);
selectCompanyContext("538880bde511f776304687e6");
if(hasRoleOsipLite().getBody()){
return cookieSession;
} else {
throw new SignInToOsipException("User doesn't has ROLE_OSIPLITE");
}
} else{
throw new SignInToOsipException("Login error, HttpSatus:"+ response.getStatusCode().toString());
}
}
private boolean isLogged(ResponseEntity response){
//if location contains '/signin', it means that there is redirect and signin is failed
return !response.getHeaders().getFirst(HttpHeaders.LOCATION).contains("osip/signin");
}
public ResponseEntity selectCompanyContext(String companyContextId){
HttpHeaders httpHeaders = makeHeadersWithJson();
HttpEntity<String> requestEntity = new HttpEntity<String>(httpHeaders);
log.debug("Selecting context... '{}' ", companyContextId);
return restTemplate.exchange(osipUrl + selectCompanyContextUrl + companyContextId, HttpMethod.GET, requestEntity, String.class);
}
public ResponseEntity<NipExistDTO> isExistNip(String nip){
HttpHeaders httpHeaders = makeHeadersWithJson();
HttpEntity<String> requestEntity = new HttpEntity<String>(httpHeaders);
log.debug("isExistTest for nip: '{}'", nip);
return restTemplate.exchange(osipUrl + existNipUrl + nip, HttpMethod.GET, requestEntity, NipExistDTO.class);
}
}
...
...
...
Controllers:
#RestController
#RequestMapping("/customer")
public class CustomerResource {
private final Logger log = LoggerFactory.getLogger(CustomerResource.class);
#Autowired
private OsipService osipService;
#RequestMapping(value = "nipExist", method = RequestMethod.GET)
public
#ResponseBody
ResponseEntity<NipExistDTO> isNipExist(#RequestParam String nip) throws SignInToOsipException {
return osipService.isExistNip(nip);
}
#RequestMapping(value = "add", method = RequestMethod.POST)
public
#ResponseBody
ResponseEntity addCustomer(#RequestBody NewCustomerDTO newCustomerDTO) throws SignInToOsipException {
return osipService.addCustomerToOsip(newCustomerDTO);
}
}
WebConfig (configuration of Session Scope)
public void onStartup(ServletContext servletContext) throws ServletException {
log.info("Web application configuration, using profiles: {}", Arrays.toString(env.getActiveProfiles()));
EnumSet<DispatcherType> disps = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC);
if (!env.acceptsProfiles(Constants.SPRING_PROFILE_FAST)) {
initMetrics(servletContext, disps);
}
if (env.acceptsProfiles(Constants.SPRING_PROFILE_PRODUCTION)) {
initCachingHttpHeadersFilter(servletContext, disps);
initStaticResourcesProductionFilter(servletContext, disps);
initGzipFilter(servletContext, disps);
}
log.info("Web application fully configured");
servletContext.addListener(new RequestContextListener());
}
AngularJS
angular.module('osipliteApp')
.controller('CustomerController', function ($rootScope, $scope, Upload, $timeout,Customer,Scenario,Dictionary,$loading,$state,Auth) {
$loading.start('addCustomer');
$scope.isCollapsed=true;
//**** Initializing fields ****//
$scope.customerDTO = {name: null, nip: null, street: null,streetNumber:null, postOffice:null, zipCode:null, phoneNumber1: null, surveyNotes:null};
$scope.personEditDTO = {name: null, email:null,code1:null, phone1:null};
$scope.newCustomerDTO = {customerType: null, scenarioId:null};
$scope.personEditDTO.code1= '+48';
$scope.customerTypes = [{name:"Osoba fizyczna",value:"NATURAL_PERSON"},{name:"Jednostka budżetowa",value:"BUDGETARY_UNITS"},{name:"Spółka prawa handlowego",value:"COMMERCIAL"},{name:"Osoba fizyczna prowadząca działalność gospodarczą",value:"NATURAL_PERSON_WITH_BUSINESS"}];
$scope.products = Dictionary.get({dictionaryCode: 'PRODUCT_TYPE',languageCode:"PL"},function(success){
$scope.scenariosList = Scenario.get({value:'active'},function(success){$loading.finish('addCustomer');},function(error){restErrorHandler(error);});
},function(error){restErrorHandler(error);});
$scope.clear = function () {
$scope.customerDTO = {name: null, nip: null, street: null,streetNumber:null, postOffice:null, zipCode:null, phoneNumber1: null, surveyNotes:null};
$scope.personEditDTO = {name: null, email:null,code1:"+48", phone1:null};
$scope.newCustomerDTO = {customerType: "NATURAL_PERSON", scenarioId:null};
$scope.nipInvalid = null;
$scope.nipExist = null;
clearSurvey();
};
...
...