I'm using spring boot to build a simple auth process for my app.
I have AuthorizationServerConfig & ResourceServerConfig setup, my frontend is a SPA. When I hit /oauth/token route, I got a JWT back which I previously stored in localStorage, and when I try to hit the resource server route, I have authorization header setup with this JWT, everything works.
But now I want to do authorization with JWT stored in the cookie, how I can config it so that it works with my current authorization/resource server config? I googled for a while and the best I can find is to set up a customize token extractor, but I'm not sure how to get it right, thank you in advance.
-------------- update --------------
I have #EnableAuthorizationServer and #EnableResourceServer on, and the EnableResourceServer setup an OAuthAuthenticationProcessingFilter automatically, this filter user bearer header authentication which uses a bearer token extractor to extract from the request header, I looked at the source code, it's hardcoded into the library, how I can customize this filter to extract JWT from the cookie?
Read cookie value from the request object and parse jwt manually.
Here is sample code
public Jws<Claims> parseJWT(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, "Token cookie name");
if(cookie == null) {
throw new SecurityException("Token not found from cookies");
}
String token = cookie.getValue();
return Jwts.parser().setSigningKey("your signing Key").parseClaimsJws(token);
}
you can create request filter and check jwt.
There are many implementation for JWT. Am using this. io.jsonwebtoken
I am adding a Token Helper Class which has methods to validate, generate, refresh token. You can focus on the JWT extraction part.
Jar Dependency
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
JWT Helper Class. It contains methods to validate, refresh, generate token.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.test.dfx.common.TimeProvider;
import com.test.dfx.model.LicenseDetail;
import com.test.dfx.model.User;
#Component
public class TokenHelper {
protected final Log LOGGER = LogFactory.getLog(getClass());
#Value("${app.name}")
private String APP_NAME;
#Value("${jwt.secret}")
public String SECRET; // Secret key used to generate Key. Am getting it from propertyfile
#Value("${jwt.expires_in}")
private int EXPIRES_IN; // can specify time for token to expire.
#Value("${jwt.header}")
private String AUTH_HEADER;
#Autowired
TimeProvider timeProvider;
private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512; // JWT Algorithm for encryption
public Date getIssuedAtDateFromToken(String token) {
Date issueAt;
try {
final Claims claims = this.getAllClaimsFromToken(token);
issueAt = claims.getIssuedAt();
} catch (Exception e) {
LOGGER.error("Could not get IssuedDate from passed token");
issueAt = null;
}
return issueAt;
}
public String getAudienceFromToken(String token) {
String audience;
try {
final Claims claims = this.getAllClaimsFromToken(token);
audience = claims.getAudience();
} catch (Exception e) {
LOGGER.error("Could not get Audience from passed token");
audience = null;
}
return audience;
}
public String refreshToken(String token) {
String refreshedToken;
Date a = timeProvider.now();
try {
final Claims claims = this.getAllClaimsFromToken(token);
claims.setIssuedAt(a);
refreshedToken = Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith( SIGNATURE_ALGORITHM, SECRET )
.compact();
} catch (Exception e) {
LOGGER.error("Could not generate Refresh Token from passed token");
refreshedToken = null;
}
return refreshedToken;
}
public String generateToken(String username) {
String audience = generateAudience();
return Jwts.builder()
.setIssuer( APP_NAME )
.setSubject(username)
.setAudience(audience)
.setIssuedAt(timeProvider.now())
.setExpiration(generateExpirationDate())
.signWith( SIGNATURE_ALGORITHM, SECRET )
.compact();
}
private Claims getAllClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.error("Could not get all claims Token from passed token");
claims = null;
}
return claims;
}
private Date generateExpirationDate() {
long expiresIn = EXPIRES_IN;
return new Date(timeProvider.now().getTime() + expiresIn * 1000);
}
public int getExpiredIn() {
return EXPIRES_IN;
}
public Boolean validateToken(String token, UserDetails userDetails) {
User user = (User) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
return (
username != null &&
username.equals(userDetails.getUsername()) &&
!isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
);
}
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
public String getToken( HttpServletRequest request ) {
/**
* Getting the token from Authentication header
* e.g Bearer your_token
*/
String authHeader = getAuthHeaderFromHeader( request );
if ( authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
public String getAuthHeaderFromHeader( HttpServletRequest request ) {
return request.getHeader(AUTH_HEADER);
}
}
Finally your controller class
public void validateToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, "TOKEN_NAME");
if(cookie == null) {
throw new SecurityException("JWT token missing");
}
String token = cookie.getValue(); // JWT Token
Claims claims = TokenHelper.getAllClaimsFromToken(token); // claims will be null if Token is invalid
}
Related
I want to implement a feature that user connects his account with external applications (similar feature is in Facebook). User has to log in to external application and grant permission to access data by my application.
Once user connected an external app, data will be exchanged in background using access and refresh tokens.
Application architecture is:
SPA front-end (Angular)
REST API (Spring), multiple nodes
ScyllaDB
Envoy proxy (with JWT verification)
The first idea is to use Spring OAuth2 Client. However, some changes need to be made:
there is no Principal because JWT is verified by Envoy proxy and X-USER-ID header is added
REST API is stateless and we shouldn't store authorization code in session
even with sessions, there are multiple nodes and we need to share authorization code between nodes
custom URL, e.g. /app_name/connect instead of /oauth2/authorization/app_name
redirect URL may be invalid (but it's verified by Spring's filter)
How this could work:
user click "Connect with app" in SPA
SPA redirects user to /oauth2/authorization/app_name (or custom URL)
Spring redirects user to external app's authentication server
user authenticates and grants permissions
external app redirects user back to Spring (or straight to SPA?)
Spring redirects user back to SPA (or SPA sends access token to REST API?)
Despite Spring Security components can be replaced, many of them are coupled and you need to rewrite OAuth2 Client flow almost from scratch. Maybe I'm doing something wrong and it can be achieved easier.
What I already did:
http
.cors().and()
.csrf().disable()
.authorizeRequests().anyRequest().permitAll().and()
.oauth2Client(); // get rid of these two filters?
#Configuration
#RequiredArgsConstructor
public class OAuth2ClientConfig {
private final CassandraTemplate cassandraTemplate;
// overriding original client service - we need to store tokens in database
#Bean
public OAuth2AuthorizedClientService authorizedClientService(
CassandraTemplate cassandraTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
return new ScyllaOAuth2AuthorizedClientService(cassandraTemplate, clientRegistrationRepository);
}
// configure client provider to use authorization code with refresh token
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
var authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.build();
var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
// the specs recommend to use WebClient for exchanging data instead of RestTemplate
#Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
// override request repository - and I'm stuck there
#Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
return new ScyllaOAuth2AuthorizationRequestRepository(cassandraTemplate);
}
}
Because there are multiple nodes of REST API, we can't use sessions. We need to store request somewhere, e.g. ScyllaDB, Redis, Hazelcast, etc. I decided to store it as JSON in ScyllaDB but I ran into trouble.
#Slf4j
#RequiredArgsConstructor
public final class ScyllaOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
private final CassandraTemplate cassandraTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
var stateParameter = this.getStateParameter(request);
if (stateParameter == null) {
return null;
}
return this.getAuthorizationRequest(request, stateParameter);
}
#Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request,
HttpServletResponse response) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
if (authorizationRequest == null) {
this.removeAuthorizationRequest(request, response);
return;
}
var state = authorizationRequest.getState();
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
Assert.hasText(state, "authorizationRequest.state cannot be empty");
try {
// serialization of Auth2AuthorizationRequest to JSON works
cassandraTemplate.getCqlOperations().execute("insert into oauth2_requests (user_id,state,data) values (?,?,?)",
userId, state, objectMapper.writeValueAsString(authorizationRequest));
} catch (JsonProcessingException e) {
log.warn("Unable to save authorization request", e);
}
}
#Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
var stateParameter = this.getStateParameter(request);
if (stateParameter == null) {
return null;
}
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
var originalRequest = this.getAuthorizationRequest(request, stateParameter);
cassandraTemplate.getCqlOperations().execute("delete from oauth2_requests where user_id=? and state=?",
userId, stateParameter);
return originalRequest;
}
private String getStateParameter(HttpServletRequest request) {
return request.getParameter(OAuth2ParameterNames.STATE);
}
private UUID getUserId(HttpServletRequest request) {
return UUID.fromString(request.getHeader(Constants.USER_ID));
}
private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request, String state) {
var userId = getUserId(request);
var jsonRequest = cassandraTemplate.getCqlOperations().queryForObject(
"select data from oauth2_requests where user_id=? and state=?", String.class, userId, state);
if (StringUtils.isNotBlank(jsonRequest)) {
try {
// trying to mess with OAuth2ClientJackson2Module
var objectMapper = new Jackson2ObjectMapperBuilder().autoDetectFields(true)
.autoDetectGettersSetters(true)
.modules(new OAuth2ClientJackson2Module())
.visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
.build();
return objectMapper.readValue(jsonRequest, OAuth2AuthorizationRequest.class);
} catch (JsonProcessingException e) {
log.warn("Error decoding authentication request", e);
}
}
return null;
}
}
I get error when trying to deserialize JSON to OAuth2AuthorizationRequest:
Missing type id when trying to resolve subtype of [simple type, class org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest]: missing type id property '#class'
Without adding OAuth2ClientJackson2Module there is another error:
Cannot construct instance of `org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
By the way, OAuth2ClientJackson2Module seems never used in original filters.
Maybe it's better to serialize this object Java way and store it as BLOB or do not store request in database but somewhere other.
Another part is the controller action:
// it had to be /apps/app_name/connect but in Spring OAuth2 Client it's hardcoded to append provider name at the end
#GetMapping("/apps/connect/app_name")
public void connect(HttpServletRequest request, HttpServletResponse response) throws IOException {
userAppService.authorize(request, response, "app_name");
}
To get rid of filters which verify redirect URL and have many things hardcoded:
#Service
#RequiredArgsConstructor
public class UserAppService {
private final HttpSecurity httpSecurity;
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private final AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
private final ClientRegistrationRepository clientRegistrationRepository;
private final OAuth2AuthorizedClientManager authorizedClientManager;
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public void authorize(HttpServletRequest request, HttpServletResponse response, String appName) throws IOException {
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
var authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(appName)
.principal(UUIDPrincipal.fromUserId(userId))
.build();
if (isAuthorizationResponse(request)) {
var authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
if (authorizationRequest != null) {
processAuthorizationRequest(request, response);
}
} else {
try {
OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
if (authorizedClient != null) {
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
System.out.println(accessToken);
}
} catch (ClientAuthorizationException e) {
// in this URL provider name is appended at the end and no way to change this behavior
var authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
"/apps/connect");
var authorizationRequest = authorizationRequestResolver.resolve(request);
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
this.redirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
}
}
}
private void processAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
var authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
var registrationId = (String) authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
var clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
var params = toMultiMap(request.getParameterMap());
var redirectUri = UrlUtils.buildFullRequestUrl(request);
var authorizationResponse = convert(params, redirectUri);
var authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
OAuth2AuthorizationCodeAuthenticationToken authenticationResult;
try {
var authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
authenticationResult = (OAuth2AuthorizationCodeAuthenticationToken) authenticationManager
.authenticate(authenticationRequest);
} catch (OAuth2AuthorizationException ex) {
OAuth2Error error = ex.getError();
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri())
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (!StringUtils.hasText(error.getDescription())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
}
if (!StringUtils.hasText(error.getUri())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_URI, error.getUri());
}
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build().encode().toString());
return;
}
// just copy-paste of original filter - trying to understand what's happening there
Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication();
String principalName = (currentAuthentication != null) ? currentAuthentication.getName() : "anonymousUser";
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), principalName, authenticationResult.getAccessToken(),
authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request,
response);
String redirectUrl = authorizationRequest.getRedirectUri();
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
}
private static boolean isAuthorizationResponse(HttpServletRequest request) {
return isAuthorizationResponseSuccess(request) || isAuthorizationResponseError(request);
}
private static boolean isAuthorizationResponseSuccess(HttpServletRequest request) {
return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.CODE))
&& StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
}
private static boolean isAuthorizationResponseError(HttpServletRequest request) {
return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.ERROR))
&& StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
}
// copy paste - not tested this code yet
static MultiValueMap<String, String> toMultiMap(Map<String, String[]> map) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(map.size());
map.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
params.add(key, value);
}
}
});
return params;
}
static OAuth2AuthorizationResponse convert(MultiValueMap<String, String> request, String redirectUri) {
String code = request.getFirst(OAuth2ParameterNames.CODE);
String errorCode = request.getFirst(OAuth2ParameterNames.ERROR);
String state = request.getFirst(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(code)) {
return OAuth2AuthorizationResponse.success(code).redirectUri(redirectUri).state(state).build();
}
String errorDescription = request.getFirst(OAuth2ParameterNames.ERROR_DESCRIPTION);
String errorUri = request.getFirst(OAuth2ParameterNames.ERROR_URI);
return OAuth2AuthorizationResponse.error(errorCode)
.redirectUri(redirectUri)
.errorDescription(errorDescription)
.errorUri(errorUri)
.state(state)
.build();
}
}
Client service to stored authorized clients in database:
#RequiredArgsConstructor
public class ScyllaOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {
private final CassandraTemplate cassandraTemplate;
private final ClientRegistrationRepository clientRegistrationRepository;
#Override
#SuppressWarnings("unchecked")
public OAuth2AuthorizedClient loadAuthorizedClient(String clientRegistrationId, String principal) {
var id = BasicMapId.id("userId", principal).with("appCode", clientRegistrationId);
var userApp = cassandraTemplate.selectOneById(id, UserApp.class);
if (userApp != null) {
var clientRegistration = getClientRegistration(clientRegistrationId);
var accessToken = getAccessToken(userApp);
var refreshToken = getRefreshToken(userApp);
return new OAuth2AuthorizedClient(clientRegistration, principal, accessToken, refreshToken);
} else {
return null;
}
}
#Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");
var userApp = new UserApp();
userApp.setUserId((UUID) principal.getPrincipal());
userApp.setAppCode(authorizedClient.getClientRegistration().getClientId());
if (authorizedClient.getAccessToken() != null) {
userApp.setAccessToken(authorizedClient.getAccessToken().getTokenValue());
userApp.setAccessTokenType(OAuth2AccessToken.TokenType.BEARER.getValue());
userApp.setAccessTokenScopes(authorizedClient.getAccessToken().getScopes());
userApp.setAccessTokenIssuedAt(authorizedClient.getAccessToken().getIssuedAt());
userApp.setAccessTokenExpiresAt(authorizedClient.getAccessToken().getExpiresAt());
}
if (authorizedClient.getRefreshToken() != null) {
userApp.setRefreshToken(authorizedClient.getRefreshToken().getTokenValue());
userApp.setRefreshTokenIssuedAt(authorizedClient.getRefreshToken().getIssuedAt());
userApp.setRefreshTokenExpiresAt(authorizedClient.getRefreshToken().getExpiresAt());
}
cassandraTemplate.insert(userApp);
}
#Override
public void removeAuthorizedClient(String clientRegistrationId, String principal) {
var id = BasicMapId.id("userId", principal).with("appCode", clientRegistrationId);
cassandraTemplate.deleteById(id, UserApp.class);
}
private ClientRegistration getClientRegistration(String clientRegistrationId) {
var clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
if (clientRegistration == null) {
throw new DataRetrievalFailureException(
"The ClientRegistration with id '" + clientRegistrationId + "' exists in the data source, "
+ "however, it was not found in the ClientRegistrationRepository.");
}
return clientRegistration;
}
private OAuth2AccessToken getAccessToken(UserApp userApp) {
return new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
userApp.getAccessToken(),
userApp.getAccessTokenIssuedAt(),
userApp.getAccessTokenExpiresAt(),
userApp.getAccessTokenScopes());
}
private OAuth2RefreshToken getRefreshToken(UserApp userApp) {
return new OAuth2RefreshToken(userApp.getRefreshToken(), userApp.getRefreshTokenIssuedAt());
}
}
Too much code overwrite. I need to make it as simple as possible.
Currently I'm struggling with storing authorize request in database.
How to do it Spring way but to keep the app architecture given at the beginning of this question?
Any way to configure OAuth2 Client without hardcoded URL like /oauth2/authorization/provider_name?
Maybe it's better to do the whole OAuth2 flow client-side (within SPA) and the SPA should send access and request token to REST API (to store the tokens in order to be able to exchange data with external app)?
In OAuth2 wording, REST APIs are resource-servers, not clients.
What you can do is have
your proxy be transparent to OAuth2 (forward requests with their JWT access-token authorization header and responses status code)
configure each REST API as resource-server. Tutorials there: https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials.
add an OAuth2 client library to your Angular app to handle tokens and authorize requests. My favorite is angular-auth-oidc-client
probably use an intermediate authorization-server for identity federation (Google, Facebook, etc., but also internal DB, LDAP, or whatever is needed), roles management, MFA,... Keycloak is a famous "on premise" solution, but you can search for "OIDC authorization-server" in your favorite search engine and have plenty of alternate choices, including SaaS like Auth0 or Amazon Cognito.
This is fully compatible with distributed architectures and micro-services (session-less is the default configuration for resource-servers in the tutorials I linked).
Two cases for a micro-service delegating some of its processing to another resource-server:
the "child" request is made on behalf of the user who initiated the request => retrieve original access-token from Authentication instance in security-context and forward it (set it as Bearer authorization header for the sub-request)
the "child" request is not made on behalf of a user => client-credentials must be used (the micro-services acquires a new access-token in its own name to authorize the sub request). Refer to spring-boot-oauth2-client and your preferred REST client docs for details (WebClient, #FeignClient, RestTemplate).
Hello I'm new to Java and Springboot. I'm currently working with an API where before making a POST request, I would need to generate a Bearer token. In order to generate a Bearer token, I would need to pass in my basic auth credentials to the "/oauth/token" endpoint. My application is having trouble passing my basic auth credentials since by the time I hit the "/v1/some-endpoint", I'm denied authorization because the Bearer token is null.
Here's my initial solution thinking I could check the url in the interceptor, then executing the following line but after debugging, it doesn't seem to be hitting that line.
Is there something I'm missing or not implementing correctly? Am I not implementing the Basic Auth endpoint correctly? Let me know if you need more information. Thanks
#Profile("!offline")
#FeignClient(
value = "someClient",
url = "${someProperty.url}",
configuration = SomeClient.SomeClientConfig.class)
public interface someClient {
#PostMapping("/v1/some-endpoint")
void redeemSomething(someRequestBody data);
#PostMapping("/oauth/token")
static BasicAuthResponse getBasicAuthToken() {
return new BasicAuthResponse();
}
#AllArgsConstructor
class SomeClientConfig extends BaseClientConfig {
private final SomeProperties properties;
private final SomeAuthTokenSupplier tokenSupplier = new SomeAuthTokenSupplier();
#Bean
#Override
public CloseableHttpClient apacheClient() {
return apacheClientFactory(properties.getUseProxy());
}
#Bean
public RequestInterceptor someAuthInterceptor() {
return template -> {
if(template.url().equals("/oauth/token")) {
String authToken = Base64Utils.encodeToString((properties.getCredentials().getUser() + ":" + properties.getCredentials().getUser()).getBytes(Charset.forName("UTF-8")));
template.header("Authorization", authToken);
}
template.header("Authorization", String.format("Bearer %s", tokenSupplier.getToken()));
};
}
private class SomeAuthTokenSupplier {
private volatile String token;
private volatile long retrievedOn = -1L;
String getToken() {
if (updateTokenRequired()) {
synchronized (this) {
if (updateTokenRequired()) {
BasicAuthResponse tokenResponse = getBasicAuthToken();
token = tokenResponse.getAccess_token(); // new token from some api should be assigned here
retrievedOn = Instant.now().toEpochMilli();
}
}
}
return token;
}
private boolean updateTokenRequired() {
return token == null || LocalDateTime.now().minusHours(8L).isAfter(LocalDateTime.ofInstant(Instant.ofEpochMilli(retrievedOn), ZoneId.systemDefault()));
}
}
#Override
public Retryer retryer() {
return new ClientRetry(250L, 2, 3) {
#Override
public void continueOrPropagate(RetryableException e) {
if (e.status() == 401 || e.status() == 403) {
tokenSupplier.token = null;
}
super.continueOrPropagate(e);
}
};
}
}
}
It worth using standard Spring Security OAuth2 Client feature instead in order to support authorization in Feign clients
See docs and code samples: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2client
UPD
See another code sample: https://github.com/int128/feign-oauth2-example
If several service endpoints require different authentication, then it's worth having several Feign clients, each with own configuration
I am building a OAuth2 authorization server using Spring Boot 2.3.1 and WebFlux. I am running into a issue. After successfully authenticating, the Authorization object is missing from the ReactiveSecurityContextHolder object.
I have an implementation of ServerSecurityContextRepository which implements the load method as below.
#Override
public Mono load(ServerWebExchange swe) {
return swe.getSession().map(WebSession::getAttributes).flatMap((attrs) -> {
ServerHttpRequest request = swe.getRequest();
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ") && authHeader.contains(" ")) {
// Split the auth header at space and the second token is the authToken.
String authToken = authHeader.split(" ")[1];
try {
// Some code removed here.
Authentication auth = new UsernamePasswordAuthenticationToken(principal, authToken);
SecurityContext sc = (SecurityContext)attrs.get(VarahamihirConstants.SECURITY_CONTEXT_ATTRIBUTE);
if (sc == null) {
sc = new SecurityContextImpl(auth);
save(swe, sc);
}
final SecurityContext context = sc;
return this.authenticationManager.authenticate(auth).map((authentication) -> {
context.setAuthentication(authentication);
return context;
});
} catch (ParseException|JOSEException|BadJOSEException e) {
return Mono.error(new UnauthorizedException("The auth token is invalid."));
}
} else {
return Mono.empty();
}
});
}
I also have an implementation of ReactiveAuthenticationManager which implements authenticate method.
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
try {
Principal principal = authentication.getPrincipal();
if (!jwtUtil.validateToken(principal.getAuthToken())) {
return Mono.empty();
}
// Some code removed.
Authentication auth = new UsernamePasswordAuthenticationToken(principal,
authToken,
actualAuthorities);
return Mono.just(auth);
})
return Mono.just(auth);
}
//SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
e.printStackTrace();
return Mono.error(e);
}
return Mono.error(new UnauthorizedException("Unreachable place."));
}
After this, the code works fine in handling the authentication flow but the ReactiveSecurityContextHolder does not contain any context. Also because of that any of the PrePost annotations can't be used.
I intuitively understand, somewhere I have to save the context into context holder but where?
I have set claims in JWT token in the token provider. now I want to get claim value through authentication when API is hit.
I have checked in Principal, details, credential, authorities but I am not getting claims in any of them.
Claims claims = Jwts.claims().setSubject(authentication.getName());
claims.put(AUTHORITIES_KEY, authorities);
claims.put("userId", userRepo.findUserIdByUsername(authentication.getName()));
return Jwts.builder()
.setSubject(authentication.getName())
.setClaims(claims)
//.claim(AUTHORITIES_KEY, authorities)
.signWith(SignatureAlgorithm.HS512, SIGNING_KEY)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000))
.compact();
I want to get "userId" claim from the authentication or any other way to get claims value from token.
This is how I read Claim from Token
private Claims getAllClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.error("Could not get all claims Token from passed token");
claims = null;
}
return claims;
}
I am using this for JWT
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
More detail here
Edit 1:
Adding Filter to get token from Request and Validate
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;
public class TokenAuthenticationFilter extends OncePerRequestFilter {
protected final Log logger = LogFactory.getLog(getClass());
private TokenHelper tokenHelper;
private UserDetailsService userDetailsService;
public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService) {
this.tokenHelper = tokenHelper;
this.userDetailsService = userDetailsService;
}
#Override
public void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException {
String username;
String authToken = tokenHelper.getToken(request);
logger.info("AuthToken: "+authToken);
if (authToken != null) {
// get username from token
username = tokenHelper.getUsernameFromToken(authToken);
logger.info("UserName: "+username);
if (username != null) {
// get user
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenHelper.validateToken(authToken, userDetails)) {
// create authentication
TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
authentication.setToken(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}else{
logger.error("Something is wrong with Token.");
}
}
chain.doFilter(request, response);
}
}
It should help.
You should be able to retrieve a claims like this within your controller
var identity = HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> claims = identity.Claims;
// or
identity.FindFirst("ClaimName").Value;
}
If you wanted, you could write extension methods for the IPrincipal interface and retrieve claims using the code above, then retrieve them using (for example)
HttpContext.User.Identity.MethodName();
For completeness of the answer. To Decode the JWT token let's write a method to validate the token and extract the information.
public static ClaimsPrincipal ValidateToken(string jwtToken)
{
IdentityModelEventSource.ShowPII = true;
SecurityToken validatedToken;
TokenValidationParameters validationParameters = new TokenValidationParameters();
validationParameters.ValidateLifetime = true;
validationParameters.ValidAudience = _audience.ToLower();
validationParameters.ValidIssuer = _issuer.ToLower();
validationParameters.IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appSettings.Secret));
ClaimsPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(jwtToken, validationParameters, out validatedToken);
return principal;
}
Now we can validate and extract the Claims by using:
ValidateToken(tokenString)?.FindFirst("ClaimName")?.Value
You should note that the ValidateToken method will return null value if the validation fails.
Using Spring Security 5 you can use #AuthenticationPrincipal org.springframework.security.oauth2.jwt.Jwt token as parameter in your controller method. And then call token.getClaims()
It would be recommended to refer the blog given below. It explained how the JWT token works in spring boot
https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
List out all the claims using JWT
private void listClaimUsingJWT(String accessToken) {
try {
SignedJWT signedJWT = SignedJWT.parse(accessToken);
JWTClaimsSet claimsSet= signedJWT.getJWTClaimsSet();
Map<String,Object> myClain =claimsSet.getClaims();
String[] keySet = myClain.keySet().toArray(new String[0]);
Log.d("JWT_Claims", "loadAllOptionalClaim JWT keySetSize "+keySet.length);
for (String s : keySet) {
Log.d("JWT_Claims", "loadAllOptionalClaim JWT key ==> " + s + " ====> " + myClain.get(s));
}
} catch (Exception e) {
e.printStackTrace();
}
}
If it is in quarkus, we can get it by injecting JSONWebToken:
/**
* Injection point for the ID Token issued by the OpenID Connect Provider
*/
#Inject
#IdToken
JsonWebToken idToken;
In Java, Keys for claim in keycloak provided by JSONWebToken can be accessed via getClaimNames() method. Following is an example:
Set<String> allClaims = this.idToken.getClaimNames();
for (String claimName: allClaims) {
System.out.println("Claim name: " + claimName);
}
Ref: https://quarkus.io/guides/security-openid-connect-web-authentication
I have an external partner that uses OAuth 1.0 to protect some resources. I need to access this resources and I would like to do this using Spring Boot and Spring Security OAuth. As I don't want to use XML configuration, I already searched for a way to set up everything via Java configuration. I found this thread that provided an example of how to do this. But serveral things regarding the OAuth 1.0 flow are not clear for me.
My partner provides four endpoints for OAuth: an endpoint that provides a consumer token, a request_token endpoint, an authorization endpoint and an access_token endpoint. With my current setup (shown below) I can get a request token and the authorization endpoint gets called. However, the authorization endpoint does not ask for confirmation, but expects as URL parameters an email and a password and, after checking the credentials, returns the following:
oauth_verifier=a02ebdc5433242e2b6e582e17b84e313
And this is where the OAuth flow gets stuck.
After reading some articles about OAuth 1.0 the usual flow is this:
get consumer token / key
get oauth token using the consumer token via request_token endpoint
redirect to authorization URL and ask the user for confirmation
redirect to consumer with verifier token
user verifier token and oauth token to get access token via access_token endpoint
First of all: steps 3 and 4 are not clear to me. I've found the Spring Security OAuth examples, but it wasn't clear to me how, after confirming the access, the user / verifier token get send back to the consumer. Could someone please explain how this is done?
Second: Given that my partners endpoint does not ask for confirmation but returns an oauth verifier right away, how can I use Spring Security OAuth with this setup? I was thinking about implementing my own authorization endpoint that calls the authorziation endpoint of my partner and then somehow makes the verifier known to my consumer, but I'm not sure how to do the latter part.
Here is the code so far (with help for the thread mentioned above; the ConsumerTokenDto has been left out as it is trivial):
Application
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Endpoint
#RestController
public class Endpoint {
#Autowired
private OAuthRestTemplate oAuthRestTemplate;
private String url = "https://....";
#RequestMapping("/public/v1/meters")
public String getMeters() {
try {
return oAuthRestTemplate.getForObject(URI.create(url), String.class);
} catch (Exception e) {
LOG.error("Exception", e);
return "";
}
}
}
OAuth configuration
#Configuration
#EnableWebSecurity
public class OAuthConfig extends WebSecurityConfigurerAdapter {
#Autowired
private RestTemplateBuilder restTemplateBuilder;
private ConsumerTokenDto consumerTokenDto;
private static final String ID = "meters";
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").permitAll();
http.addFilterAfter(this.oauthConsumerContextFilter(), SwitchUserFilter.class);
http.addFilterAfter(this.oauthConsumerProcessingFilter(), OAuthConsumerContextFilterImpl.class);
}
private OAuthConsumerContextFilter oauthConsumerContextFilter() {
OAuthConsumerContextFilter filter = new OAuthConsumerContextFilter();
filter.setConsumerSupport(this.consumerSupport());
return filter;
}
private OAuthConsumerProcessingFilter oauthConsumerProcessingFilter() {
OAuthConsumerProcessingFilter filter = new OAuthConsumerProcessingFilter();
filter.setProtectedResourceDetailsService(this.prds());
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> map = new LinkedHashMap<>();
// one entry per oauth:url element in xml
map.put(
new AntPathRequestMatcher("/public/v1/**", null),
Collections.singletonList(new SecurityConfig(ID)));
filter.setObjectDefinitionSource(new DefaultFilterInvocationSecurityMetadataSource(map));
return filter;
}
#Bean
OAuthConsumerSupport consumerSupport() {
CoreOAuthConsumerSupport consumerSupport = new CoreOAuthConsumerSupport();
consumerSupport.setProtectedResourceDetailsService(prds());
return consumerSupport;
}
#Bean
ProtectedResourceDetailsService prds() {
InMemoryProtectedResourceDetailsService service = new InMemoryProtectedResourceDetailsService();
Map<String, ProtectedResourceDetails> store = new HashMap<>();
store.put(ID, prd());
service.setResourceDetailsStore(store);
return service;
}
ProtectedResourceDetails prd() {
ConsumerTokenDto consumerToken = getConsumerToken();
BaseProtectedResourceDetails resourceDetails = new BaseProtectedResourceDetails();
resourceDetails.setId(ID);
resourceDetails.setConsumerKey(consumerToken.getKey());
resourceDetails.setSharedSecret(new SharedConsumerSecretImpl(consumerToken.getSecret()));
resourceDetails.setRequestTokenURL("https://.../request_token");
// the authorization URL does not prompt for confirmation but immediately returns an OAuth verifier
resourceDetails.setUserAuthorizationURL(
"https://.../authorize?email=mail&password=pw");
resourceDetails.setAccessTokenURL("https://.../access_token");
resourceDetails.setSignatureMethod(HMAC_SHA1SignatureMethod.SIGNATURE_NAME);
return resourceDetails;
}
// get consumer token from provider
private ConsumerTokenDto getConsumerToken() {
if (consumerTokenDto == null) {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("client", "Client");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
RestTemplate restTemplate = restTemplateBuilder.setConnectTimeout(1000).setReadTimeout(1000).build();
restTemplate.getInterceptors().add(interceptor);
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
ResponseEntity<ConsumerTokenDto> response = restTemplate
.exchange("https://.../consumer_token", HttpMethod.POST, request,
ConsumerTokenDto.class);
consumerTokenDto = response.getBody();
}
return consumerTokenDto;
}
// create oauth rest template
#Bean
public OAuthRestTemplate oAuthRestTemplate() {
OAuthRestTemplate oAuthRestTemplate = new OAuthRestTemplate(prd());
oAuthRestTemplate.getInterceptors().add(interceptor);
return oAuthRestTemplate;
}
}
I think I've found a solution. The trick is to implement my own OAuthConsumerContextFilter and replace the redirect call with a direct call to the authorization endpoint. I've commented the interesting parts below (starting with //!!!!).
CustomOAuthConsumerContextFilter
public class CustomOAuthConsumerContextFilter extends OAuthConsumerContextFilter {
private static final Logger LOG = LoggerFactory.getLogger(CustomOAuthConsumerContextFilter.class);
private RestTemplateBuilder restTemplateBuilder;
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
OAuthSecurityContextImpl context = new OAuthSecurityContextImpl();
context.setDetails(request);
Map<String, OAuthConsumerToken> rememberedTokens =
getRememberMeServices().loadRememberedTokens(request, response);
Map<String, OAuthConsumerToken> accessTokens = new TreeMap<>();
Map<String, OAuthConsumerToken> requestTokens = new TreeMap<>();
if (rememberedTokens != null) {
for (Map.Entry<String, OAuthConsumerToken> tokenEntry : rememberedTokens.entrySet()) {
OAuthConsumerToken token = tokenEntry.getValue();
if (token != null) {
if (token.isAccessToken()) {
accessTokens.put(tokenEntry.getKey(), token);
} else {
requestTokens.put(tokenEntry.getKey(), token);
}
}
}
}
context.setAccessTokens(accessTokens);
OAuthSecurityContextHolder.setContext(context);
if (LOG.isDebugEnabled()) {
LOG.debug("Storing access tokens in request attribute '" + getAccessTokensRequestAttribute() + "'.");
}
try {
try {
request.setAttribute(getAccessTokensRequestAttribute(), new ArrayList<>(accessTokens.values()));
chain.doFilter(request, response);
} catch (Exception e) {
try {
ProtectedResourceDetails resourceThatNeedsAuthorization = checkForResourceThatNeedsAuthorization(e);
String neededResourceId = resourceThatNeedsAuthorization.getId();
//!!!! store reference to verifier here, outside of loop
String verifier = null;
while (!accessTokens.containsKey(neededResourceId)) {
OAuthConsumerToken token = requestTokens.remove(neededResourceId);
if (token == null) {
token = getTokenServices().getToken(neededResourceId);
}
// if the token is null OR
// if there is NO access token and (we're not using 1.0a or the verifier is not null)
if (token == null || (!token.isAccessToken() &&
(!resourceThatNeedsAuthorization.isUse10a() || verifier == null))) {
//no token associated with the resource, start the oauth flow.
//if there's a request token, but no verifier, we'll assume that a previous oauth request failed and we need to get a new request token.
if (LOG.isDebugEnabled()) {
LOG.debug("Obtaining request token for resource: " + neededResourceId);
}
//obtain authorization.
String callbackURL = response.encodeRedirectURL(getCallbackURL(request));
token = getConsumerSupport().getUnauthorizedRequestToken(neededResourceId, callbackURL);
if (LOG.isDebugEnabled()) {
LOG.debug("Request token obtained for resource " + neededResourceId + ": " + token);
}
//okay, we've got a request token, now we need to authorize it.
requestTokens.put(neededResourceId, token);
getTokenServices().storeToken(neededResourceId, token);
String redirect =
getUserAuthorizationRedirectURL(resourceThatNeedsAuthorization, token, callbackURL);
if (LOG.isDebugEnabled()) {
LOG.debug("Redirecting request to " + redirect +
" for user authorization of the request token for resource " +
neededResourceId + ".");
}
request.setAttribute(
"org.springframework.security.oauth.consumer.AccessTokenRequiredException", e);
// this.redirectStrategy.sendRedirect(request, response, redirect);
//!!!! get the verifier from the authorization URL
verifier = this.getVerifier(redirect);
//!!!! start next iteration of loop -> now we have the verifier, so the else statement below shoud get executed and an access token retrieved
continue;
} else if (!token.isAccessToken()) {
//we have a presumably authorized request token, let's try to get an access token with it.
if (LOG.isDebugEnabled()) {
LOG.debug("Obtaining access token for resource: " + neededResourceId);
}
//authorize the request token and store it.
try {
token = getConsumerSupport().getAccessToken(token, verifier);
} finally {
getTokenServices().removeToken(neededResourceId);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Access token " + token + " obtained for resource " + neededResourceId +
". Now storing and using.");
}
getTokenServices().storeToken(neededResourceId, token);
}
accessTokens.put(neededResourceId, token);
try {
//try again
if (!response.isCommitted()) {
request.setAttribute(getAccessTokensRequestAttribute(),
new ArrayList<>(accessTokens.values()));
chain.doFilter(request, response);
} else {
//dang. what do we do now?
throw new IllegalStateException(
"Unable to reprocess filter chain with needed OAuth2 resources because the response is already committed.");
}
} catch (Exception e1) {
resourceThatNeedsAuthorization = checkForResourceThatNeedsAuthorization(e1);
neededResourceId = resourceThatNeedsAuthorization.getId();
}
}
} catch (OAuthRequestFailedException eo) {
fail(request, response, eo);
} catch (Exception ex) {
Throwable[] causeChain = getThrowableAnalyzer().determineCauseChain(ex);
OAuthRequestFailedException rfe = (OAuthRequestFailedException) getThrowableAnalyzer()
.getFirstThrowableOfType(OAuthRequestFailedException.class, causeChain);
if (rfe != null) {
fail(request, response, rfe);
} else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
} else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. These are not expected to happen
throw new RuntimeException(ex);
}
}
}
} finally {
OAuthSecurityContextHolder.setContext(null);
HashMap<String, OAuthConsumerToken> tokensToRemember = new HashMap<>();
tokensToRemember.putAll(requestTokens);
tokensToRemember.putAll(accessTokens);
getRememberMeServices().rememberTokens(tokensToRemember, request, response);
}
}
private String getVerifier(String authorizationURL) {
HttpEntity request = HttpEntity.EMPTY;
RestTemplate restTemplate = restTemplateBuilder.setConnectTimeout(1000).setReadTimeout(1000).build();
ResponseEntity<String> response =
restTemplate.exchange(authorizationURL, HttpMethod.GET, request, String.class);
//!!!! extract verifier from response
String verifier = response.getBody().split("=")[1];
return verifier;
}
void setRestTemplateBuilder(RestTemplateBuilder restTemplateBuilder) {
this.restTemplateBuilder = restTemplateBuilder;
}
}