I am working with Spring boot and Spring security to build a back end application, I am storing users into Cloud Firestore NonRelational Database, I am using an Admin SDK token provided by Google on the Firebase platform. I am initializing my Firestore in the following way.
#Service
public class UserFirestoreInitialize {
#Value("classpath:static/gamingplatform-c922d-firebase-adminsdk-c25o8-06e92edfd5.json")
Resource resourceFile;
#PostConstruct
public void initialize() {
try {
InputStream serviceAccount = resourceFile.getInputStream();
GoogleCredentials cred = GoogleCredentials.fromStream(serviceAccount)
.createScoped("https://www.googleapis.com/auth/datastore");
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(cred)
.setDatabaseUrl("FIREBASE_URL")
.build();
FirebaseApp.initializeApp(options);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
I also had a Configuration class which looks like this.
#Configuration
public class UserFirestoreConfiguration{
#Bean
public Firestore getFirestore(){
return FirestoreClient.getFirestore();
}
}
After this, I could easily use this bean in my UserService as follows:
#Service
public class UserService{
#Autowired
private Firestore firestore;
public User getUser(){
//Query for user.
}
//Post, Put, delete
}
This worked at some point. The problem came when I added Spring Security into my applicatio. When I ran a Maven Install, the application did not build, the problem looks like this:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jwtTokenFilter': Unsatisfied dependency expressed through field 'userDetailsService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userServiceDetailsImpl' defined in file [/Users/igorzelaya/SoftwareDev/D1Gaming-User-Back-end1/target/classes/com/d1gaming/user/security/UserServiceDetailsImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in file [/Users/igorzelaya/SoftwareDev/D1Gaming-User-Back-end1/target/classes/com/d1gaming/user/user/UserService.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.d1gaming.user.user.UserService]: Constructor threw exception; nested exception is java.lang.IllegalStateException: FirebaseApp with name [DEFAULT] doesn't exist.
I have a tried to isolate the problem, I created the another project and tried connecting to my database like I did in the snippet above^, everything worked just fine, I then copied my configuration classes one by one, I figured my application started to crash the moment I added my JWT Token Filter class, this class looks like this
#Component
public class JwtTokenFilter extends OncePerRequestFilter{
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
private UserServiceDetailsImpl userDetailsService;
#Override
//Get authorization header and validate it.
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException, NullPointerException {
try {
String jwt = parseJwt(request);
if(jwt != null && jwtTokenUtil.validate(jwt)) {
String username = jwtTokenUtil.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
catch(Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
chain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if(StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
}
As you can notice on my stack trace shown above, the error starts on the dependencies injected in this class, the jwtTokenUtil class and the UserDetailsImpl
the JwtTokenUtil class is this:
#Component
public class JwtTokenUtil {
#Value("${app.jwtSecret}")
private String jwtSecret;
#Value("${app.jwtExpirationMs}")
private int jwtExpirationMs;
private final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret)
.parseClaimsJws(token).getBody().getSubject();
}
public String getUserId(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject().split(",")[0];
}
public String getUsername(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject().split(",")[0];
}
public Date getExpirationDate(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getExpiration();
}
public boolean validate(String token) {
try {
Jwts.parser()
.setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
}
catch(SignatureException e) {
logger.error("Invalid JWT signature - {}",e.getMessage());
}
catch(MalformedJwtException e) {
logger.error("Invalid JWT token - {}", e.getMessage());
}
catch(ExpiredJwtException e) {
logger.error("Invalid JWT token - {}",e.getMessage());
}
catch(UnsupportedJwtException e) {
logger.error("Invalid JWT token - {}", e.getMessage());
}
catch(IllegalArgumentException e) {
logger.error("Invalid JWT token - {}", e.getMessage());
}
return false;
}
}
The UserServiceDetailsImpl class:
public class UserDetailsImpl implements UserDetails{
private static final long serialVersionUID = 1L;
private String userId;
private String userRealName;
private String userName;
#JsonIgnore
private String userPassword;
private String userEmail;
private UserStatus userStatusCode;
private Team userTeam;
private Map<String,Object> userBilling;
private String userCountry;
private int userTokens;
private double userCash;
private Map<String, Object> userBirthDate;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(String userId, String userRealName, String userName, String userPassword, String userEmail,
UserStatus userStatusCode, Team userTeam, Map<String, Object> userBilling, String userCountry,
int userTokens, double userCash, Map<String, Object> userBirthDate,
Collection<? extends GrantedAuthority> authorities) {
this.userId = userId;
this.userRealName = userRealName;
this.userName = userName;
this.userPassword = userPassword;
this.userEmail = userEmail;
this.userStatusCode = userStatusCode;
this.userTeam = userTeam;
this.userBilling = userBilling;
this.userCountry = userCountry;
this.userTokens = userTokens;
this.userCash = userCash;
this.userBirthDate = userBirthDate;
this.authorities = authorities;
}
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = user.getUserRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleType().name()))
.collect(Collectors.toList());
return new UserDetailsImpl(user.getUserId(),user.getUserRealName(),user.getUserName(),user.getUserPassword(),user.getUserEmail()
,user.getStatusCode(),user.getUserTeam(),user.getUserBilling(),user.getUserCountry(),
user.getUserTokens(),user.getUserCash(),user.getUserBirthDate(),authorities
);
}
//Getters and setters
}
As you can see, these classes both have nothing to do with Firestore, At the moment I was pretty confused so I did the following. I noticed that for some strange reason the #PostConstruct annotation was not working, Spring was not initializing my initializeFirestore() method on the first snippet of code, so I placed a initialize() method directly on spring boot as follows:
#SpringBootApplication
public class UserApplication(){
public static void main(String[] args) {
//Basically does the same as the initialize method mentioned before.
UserFirestoreUtils.initialize(UserFirestoreUtils.getOptions());
SpringApplication.run(D1GamingUserBackEnd1Application.class, args);
}
}
The interesting thing is that when I debugged this I had the following error:
The dependencies of some of the beans in the application context form a cycle:
jwtTokenFilter (field private com.d1gaming.user.security.UserServiceDetailsImpl com.d1gaming.user.security.JwtTokenFilter.userDetailsService)
┌─────┐
| userServiceDetailsImpl defined in file [/Users/igorzelaya/SoftwareDev/D1Gaming-User-Back-end1/target/classes/com/d1gaming/user/security/UserServiceDetailsImpl.class]
↑ ↓
| userService (field private org.springframework.security.crypto.password.PasswordEncoder com.d1gaming.user.user.UserService.passwordEncoder)
↑ ↓
| userSecurityConfiguration (field com.d1gaming.user.security.UserServiceDetailsImpl com.d1gaming.user.security.UserSecurityConfiguration.userDetailsService)
└─────┘
The weird thing is that when I run a Maven install on it, the stack trace is the same as the one I showed at the beginning of the question, I feel there is not enough documentation on Cloud Firestore and its normal considering it is "New", but still it was annoying because I couldn't find the "Right way" of doing this and Google Documentation was clearly not enough. I am sorry If I included way too much code, I think it is necessary, but anyways thank you for your time, I appreciate if anyone could help me. Have a nice day.
This looks like a collision between Spring dependencies and Firebase Admin. You can try with Firestore client library. Firebase admin SDK is a wider library that some part of code can collision with Spring security
Related
I have a spring service:
#Service
public class AuthorizationServiceImpl implements AuthorizationService {
#Value("${local.address}")
private String localIP;
private final String LOCALHOST_IPV4 = "127.0.0.1";
private final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
#CrossOrigin(origins = "*")
#Override
public boolean isAuthorized(String token, HttpServletRequest request) {
try {
TokenUserModel user = new TokenUserModel();
user.setSessionID("");
user = getTokenUserModel(token);
IServiceAAA client = new ServiceAAA().getBasicHttpBindingIServiceAAA();
int systemId = client.getUserSystemId("InvoiceAdmin");
String xRealIP = request.getRemoteAddr();
String ipAddress = xRealIP.equals(LOCALHOST_IPV4) || xRealIP.equals(LOCALHOST_IPV6) ? localIP : xRealIP;
String userAgent = request.getHeader("User-Agent");
int response = client.checkPermissionAndUserData(user.getSessionID(), "Admin",
userAgent,
ipAddress, systemId, systemId, "");
return response == 0;
} catch (Exception ex) {
throw new AuthorizationException(AuthorizationError.ERR_WHILE_AUTHORISATION);
}
}
}
#Value("${local.address}")
private String localIP; - this line of code didn't working, not populating local.address field from application.properties:
local.address=10.10.16.13
Update:
I have 3 application properties file for spring profiles, named:
application.properties,application-dev.properties,application-prod.properties,
my current active profile is application-dev.properties
('spring.profiles.active=dev' - it's inside of my
application.properties file).
I'm sure that 'local.address'
property is defined in application-dev.properties
local.address=10.10.16.13
AuthorizationService is a just interface that AutheroziationServiceImpl impliments.
public interface AuthorizationService {
boolean isAuthorized(String token, HttpServletRequest request) throws UnsupportedEncodingException;
void logout(String token) throws UnsupportedEncodingException;
TokenUserModel getTokenUserModel(String token) throws UnsupportedEncodingException;
}
I'm using it in constructor of one of controllers:
#Autowired
public AuthorizationController(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
private final AuthorizationService authorizationService;
Yes, it's spring managed bean annotated as #Service
I'm using azure keyvault to pull my application properties. I'm using spring #value annotation to set the property value from the keyvault by placing the placeholder in the application.properties file. In my main application context I was able to pull the properties and test the application flow. Were as in test context its throwing some issuing saying vault properties aren't injected. Here is my properties bean class looks like, and the stack trace of the issue. I tried to mock the KeyVaultProperties in the ControllerTest class still having same issue.
KeyVault.java
#Data
#Component
public class KeyVaultProperties {
#Value("${by-pass-token}")
private String token;
#Value("${backend-clients}")
private String clients;
}
ControllerTest.java
#SpringBootTest
#SpringBootConfiguration
#AutoConfigureMockMvc
public class ControllerTest {
#Autowired
Controller controller;
#Autowired
private MockMvc mockMvc;
#Test
public void contextLoads() throws Exception {
assertThat(controller).isNotNull();
}
}
Controller.java
#RestController
#Slf4j
#RequestMapping("/api/test")
public class Controller {
#GetMapping(value = "/hello")
public String getString() {
return "Hello";
}
}
AuthConfiguration.java
#Slf4j
#Component
public class AuthConfiguration extends HandlerInterceptorAdapter {
#Autowired
private KeyVaultProperties keyVaultProperties;
private static final String CORRELATION_ID_LOG_VAR_NAME = "correlationId";
private static final String CORRELATION_ID_HEADER_NAME = "Correlation-Id";
#PostConstruct
public void setup() {
System.out.println("-------#PostConstruct------setup----------------");
sub = keyVaultProperties.getClients();
ByPass = keyVaultProperties.getAuthByPassToken();
}
#Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler)
throws Exception {
System.out.println("-------preHandle----------------------");
final Boolean isValidToken;
final String correlationId = getCorrelationIdFromHeader(request);
log.info("correlationId:{}",correlationId);
MDC.put(CORRELATION_ID_LOG_VAR_NAME, correlationId);
return true;
}
#Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final Exception ex) {
System.out.println("-------afterCompletion----------------------");
MDC.remove(CORRELATION_ID_LOG_VAR_NAME);
}
private String getCorrelationIdFromHeader(final HttpServletRequest request) {
String correlationId = request.getHeader(CORRELATION_ID_HEADER_NAME);
if (correlationId == null) {
correlationId = generateUniqueCorrelationId();
}
return correlationId;
}
}
app/src/main/resources/application.properties
by-pass-token = ${BY-PASS-TOKEN}
backend-clients = ${CLIENTS}
azure.keyvault.enabled=true
Stack Trace:
2021-04-04 13:28:03.640 [main] ERROR org.springframework.boot.SpringApplication - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'AuthConfiguration': Unsatisfied dependency expressed through field 'KeyVaultProperties'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'KeyVaultProperties': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'by-pass-token' in value "${by-pass-token}"
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject
You could set the value of properties to Azure Key Vault by authenticating via Azure AD.
Note: In order for your application to have access to the Key Vault contents, you must set the appropriate permissions for your application in the Key Vault. Navigate to Azure Key Vault > Access Policies > Add access policy > select your application in select principal.
Dependencies:
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-keyvault</artifactId>
<version>1.0.0</version>
</dependency>
Connect to key vault via AzureAD based on client credentials flow:
public class ClientSecretKeyVaultCredential extends KeyVaultCredentials
{
private String clientId;
private String clientKey;
public ClientSecretKeyVaultCredential( String clientId, String clientKey ) {
this.clientId = clientId;
this.clientKey = clientKey;
}
#Override
public String doAuthenticate(String authorization, String resource, String scope) {
AuthenticationResult token = getAccessTokenFromClientCredentials(
authorization, resource, clientId, clientKey);
return token.getAccessToken();
}
private static AuthenticationResult getAccessTokenFromClientCredentials(
String authorization, String resource, String clientId, String clientKey) {
AuthenticationContext context = null;
AuthenticationResult result = null;
ExecutorService service = null;
try {
service = Executors.newFixedThreadPool(1);
context = new AuthenticationContext(authorization, false, service);
ClientCredential credentials = new ClientCredential(clientId, clientKey);
Future<AuthenticationResult> future = context.acquireToken(
resource, credentials, null);
result = future.get();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
service.shutdown();
}
if (result == null) {
throw new RuntimeException("authentication result was null");
}
return result;
}
}
Access Key vault:
You could use client.setSecret("Secret-Name", "value") to set your properties.
// ClientSecretKeyVaultCredential is the implementation of KeyVaultCredentials
KeyVaultClient client = new KeyVaultClient(
new ClientSecretKeyVaultCredential(clientId, clientKey));
// KEYVAULT_URL is the location of the keyvault to use: https://<yourkeyvault>.vault.azure.net
SecretBundle secret = client.getSecret( KEYVAULT_URL, "Secret-name" );
log( secret.value() );
I'm trying to get a new access token using a refresh token in Spring Boot with OAuth2. It should be done as following: POST: url/oauth/token?grant_type=refresh_token&refresh_token=....
It works fine if I'm using InMemoryTokenStore because the token is tiny and contains only digits/letters but right now I'm using a JWT token and as you probably know it has 3 different parts which probably are breaking the code.
I'm using the official migration guide to 2.4.
When I try to access the URL above, I'm getting the following message:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
How do I pass a JWT token in the params? I tried to set a breakpoint on that message, so I could see what the actual argument was, but it didn't get to it for some reason.
/**
* The Authorization Server is responsible for generating tokens specific to a client.
* Additional information can be found here: https://www.devglan.com/spring-security/spring-boot-security-oauth2-example.
*/
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Value("${user.oauth2.client-id}")
private String clientId;
#Value("${user.oauth2.client-secret}")
private String clientSecret;
#Value("${user.oauth2.accessTokenValidity}")
private int accessTokenValidity;
#Value("${user.oauth2.refreshTokenValidity}")
private int refreshTokenValidity;
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient(clientId)
.secret(bCryptPasswordEncoder.encode(clientSecret))
.authorizedGrantTypes("password", "authorization_code", "refresh_token")
.scopes("read", "write", "trust")
.resourceIds("api")
.accessTokenValiditySeconds(accessTokenValidity)
.refreshTokenValiditySeconds(refreshTokenValidity);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
.userApprovalHandler(userApprovalHandler())
.accessTokenConverter(accessTokenConverter());
}
#Bean
public UserApprovalHandler userApprovalHandler() {
ApprovalStoreUserApprovalHandler userApprovalHandler = new ApprovalStoreUserApprovalHandler();
userApprovalHandler.setApprovalStore(approvalStore());
userApprovalHandler.setClientDetailsService(clientDetailsService);
userApprovalHandler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
return userApprovalHandler;
}
#Bean
public TokenStore tokenStore() {
JwtTokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
tokenStore.setApprovalStore(approvalStore());
return tokenStore;
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final RsaSigner signer = new RsaSigner(KeyConfig.getSignerKey());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
private JsonParser objectMapper = JsonParserFactory.create();
#Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper.formatMap(getAccessTokenConverter().convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException("Cannot convert access token to JSON", ex);
}
Map<String, String> headers = new HashMap<>();
headers.put("kid", KeyConfig.VERIFIER_KEY_ID);
return JwtHelper.encode(content, signer, headers).getEncoded();
}
};
converter.setSigner(signer);
converter.setVerifier(new RsaVerifier(KeyConfig.getVerifierKey()));
return converter;
}
#Bean
public ApprovalStore approvalStore() {
return new InMemoryApprovalStore();
}
#Bean
public JWKSet jwkSet() {
RSAKey.Builder builder = new RSAKey.Builder(KeyConfig.getVerifierKey())
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID(KeyConfig.VERIFIER_KEY_ID);
return new JWKSet(builder.build());
}
}
I assume that the Cannot convert access token to JSON might have been due to incorrectly pasted token.
As for Invalid refresh token, it occurs because when JwtTokenStore reads the refresh token, it validates the scopes and revocation with InMemoryApprovalStore. However, for this implementation, the approvals are registered only during authorization through /oauth/authorize URL (Authorisation Code Grant) by the ApprovalStoreUserApprovalHandler.
Especially for the Authorisation Code Grant (authorization_code), you want to have this validation, so that the refresh token request will not be called with an extended scope without the user knowledge. Moreover, it's optional to store approvals for future revocation.
The solution is to fill the ApprovalStore with the Approval list for all resource owners either statically or dynamically. Additionally, you might be missing setting the user details service endpoints.userDetailsService(userDetailsService) which is used during the refresh process.
Update:
You can verify this by creating pre-filled InMemoryApprovalStore:
#Bean
public ApprovalStore approvalStore() {
InMemoryApprovalStore approvalStore = new InMemoryApprovalStore();
Date expirationDate = Date.from(Instant.now().plusSeconds(3600));
List<Approval> approvals = Stream.of("read", "write", "trust")
.map(scope -> new Approval("admin", "trusted", scope, expirationDate,
ApprovalStatus.APPROVED))
.collect(Collectors.toList());
approvalStore.addApprovals(approvals);
return approvalStore;
}
I would also take a look at implementing it in the storeRefreshToken()/storeAccessToken() methods of JwtTokenStore, as they have an empty implementation, and the method parameters contain all the necessary data.
I'm updating an old application to use WebFlux but I've gotten a bit lost when it comes to handling JWT validation with Spring Security.
The existing code (which works with standard Spring Web) looks like:
(Validating a Firebase Token)
public class FirebaseAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter {
private static final String TOKEN_HEADER = "X-Firebase-Auth";
public FirebaseAuthenticationTokenFilter() {
super("/v1/**");
}
#Override
public Authentication attemptAuthentication(
final HttpServletRequest request, final HttpServletResponse response) {
for (final Enumeration<?> e = request.getHeaderNames(); e.hasMoreElements(); ) {
final String nextHeaderName = (String) e.nextElement();
final String headerValue = request.getHeader(nextHeaderName);
}
final String authToken = request.getHeader(TOKEN_HEADER);
if (Strings.isNullOrEmpty(authToken)) {
throw new RuntimeException("Invaild auth token");
}
return getAuthenticationManager().authenticate(new FirebaseAuthenticationToken(authToken));
}
However when switching to WebFlux we lose HttpServletRequest and HttpServletResponse. There is a GitHub issue which suggests there is an alternative method/fix https://github.com/spring-projects/spring-security/issues/5328 however following it through I'm not able to identify what was actually changed to make this work.
The Spring Security docs while great, don't really explain how to handle the use-case.
Any tips on how to proceed?
Got there in the end:
First need to update the filter chain with a custom filter just like before
#Configuration
public class SecurityConfig {
private final FirebaseAuth firebaseAuth;
public SecurityConfig(final FirebaseAuth firebaseAuth) {
this.firebaseAuth = firebaseAuth;
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
http.authorizeExchange()
.and()
.authorizeExchange()
.pathMatchers("/v1/**")
.authenticated()
.and()
.addFilterAt(firebaseAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.csrf()
.disable();
return http.build();
}
private AuthenticationWebFilter firebaseAuthenticationFilter() {
final AuthenticationWebFilter webFilter =
new AuthenticationWebFilter(new BearerTokenReactiveAuthenticationManager());
webFilter.setServerAuthenticationConverter(new FirebaseAuthenticationConverter(firebaseAuth));
webFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/v1/**"));
return webFilter;
}
}
The main workhorse of the process is FirebaseAuthenticationConverter where I validate the incoming JWT against Firebase, and perform some standard logic against it.
#Slf4j
#Component
#RequiredArgsConstructor
public class FirebaseAuthenticationConverter implements ServerAuthenticationConverter {
private static final String BEARER = "Bearer ";
private static final Predicate<String> matchBearerLength =
authValue -> authValue.length() > BEARER.length();
private static final Function<String, Mono<String>> isolateBearerValue =
authValue -> Mono.justOrEmpty(authValue.substring(BEARER.length()));
private final FirebaseAuth firebaseAuth;
private Mono<FirebaseToken> verifyToken(final String unverifiedToken) {
try {
final ApiFuture<FirebaseToken> task = firebaseAuth.verifyIdTokenAsync(unverifiedToken);
return Mono.justOrEmpty(task.get());
} catch (final Exception e) {
throw new SessionAuthenticationException(e.getMessage());
}
}
private Mono<FirebaseUserDetails> buildUserDetails(final FirebaseToken firebaseToken) {
return Mono.just(
FirebaseUserDetails.builder()
.email(firebaseToken.getEmail())
.picture(firebaseToken.getPicture())
.userId(firebaseToken.getUid())
.username(firebaseToken.getName())
.build());
}
private Mono<Authentication> create(final FirebaseUserDetails userDetails) {
return Mono.justOrEmpty(
new UsernamePasswordAuthenticationToken(
userDetails.getEmail(), null, userDetails.getAuthorities()));
}
#Override
public Mono<Authentication> convert(final ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange)
.flatMap(AuthorizationHeaderPayload::extract)
.filter(matchBearerLength)
.flatMap(isolateBearerValue)
.flatMap(this::verifyToken)
.flatMap(this::buildUserDetails)
.flatMap(this::create);
}
}
To the previous answer there could be added that this method also works fine:
private Mono<FirebaseToken> verifyToken(final String unverifiedToken) {
try {
return Mono.just(FirebaseAuth.getInstance().verifyIdToken(unverifiedToken));
} catch (final Exception e) {
throw new SessionAuthenticationException(e.getMessage());
}
}
And this one does not provid warnings regarding unnecessary use of blocking methods (like get())
I have successfully integrated Spring Security OAuth2 with my Open ID Connect provider (Forgerock OpenAM). I can see the access token being retrieved. How can I access the id_token and refresh_token which are part of the response from the /token endpoint?
Finally figured out answer and posting in case it is useful for someone with the same problem. After session is authenticated by Spring Security OAuth2, there is an Authentication Object setup. It needs to get casted to an instance of OAuth2Authentication. That object has the token.
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof OAuth2Authentication) {
Object details = auth.getDetails();
OAuth2AccessToken token = oauth2Ctx.getAccessToken();
if (token != null && !token.isExpired()) {
// Do Stuff
}
A complete example of an alternative approach (using Spring Boot and disabling part of its autoconfiguration).
application.properties:
security.oauth2.client.client-id=client-id
security.oauth2.client.client-secret=client-secret
security.oauth2.client.access-token-uri=http://my-oidc-provider/auth/oauth2/token
security.oauth2.client.user-authorization-uri=http://my-oidc-provider/auth/oauth2/authorize
security.oauth2.resource.token-info-uri=http://my-oidc-provider/auth/oauth2/check_token
security.oauth2.client.scope=openid,email,profile
security.oauth2.resource.jwk.key-set-uri=http://my-oidc-provider/auth/oidc/jwks
/**
* Extending the AuthorizationServerEndpointsConfiguration disables the Spring
* Boot ResourceServerTokenServicesConfiguration.
*/
#Configuration
#EnableOAuth2Sso
public class OAuth2Config extends AuthorizationServerEndpointsConfiguration {
#Value("${security.oauth2.resource.jwk.key-set-uri}")
private String keySetUri;
#Value("${security.oauth2.resource.token-info-uri}")
private String checkTokenEndpointUrl;
#Value("${security.oauth2.client.client-id}")
private String clientId;
#Value("${security.oauth2.client.client-secret}")
private String clientSecret;
#Bean
public RemoteTokenServices resourceServerTokenServices() {
RemoteTokenServices tokenService = new RemoteTokenServices();
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(new CustomIdTokenConverter(keySetUri));
tokenService.setAccessTokenConverter(accessTokenConverter);
tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
tokenService.setClientId(clientId);
tokenService.setClientSecret(clientSecret);
return tokenService;
}
#Bean
public ClientDetailsService clientDetailsService() {
return new InMemoryClientDetailsService();
}
#Bean
public UserInfoRestTemplateFactory userInfoRestTemplateFactory(
ObjectProvider<List<UserInfoRestTemplateCustomizer>> customizers,
ObjectProvider<OAuth2ProtectedResourceDetails> details,
ObjectProvider<OAuth2ClientContext> oauth2ClientContext) {
return new DefaultUserInfoRestTemplateFactory(customizers, details,
oauth2ClientContext);
}
}
public class CustomIdTokenConverter extends DefaultUserAuthenticationConverter {
private final JwkTokenStore jwkTokenStore;
public CustomIdTokenConverter(String keySetUri) {
this.jwkTokenStore = new JwkTokenStore(keySetUri);
}
#Override
public Authentication extractAuthentication(Map<String, ?> map) {
String idToken = (String) map.get("id_token");
OAuth2AccessToken token = jwkTokenStore.readAccessToken(idToken);
Map<String, Object> claims = token.getAdditionalInformation();
OAuth2RefreshToken refreshToken = token.getRefreshToken();
String principal = (String) claims.get("sub");
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
return new CustomAuthenticationData(principal, claims, authorities);
}
}
public class CustomAuthenticationData extends UsernamePasswordAuthenticationToken {
private final Map<String, Object> attributes;
public CustomAuthenticationData(String username, Map<String, Object> attributes, Collection<? extends GrantedAuthority> authorities) {
super(username, "N/A", authorities);
this.attributes = attributes;
}
public Map<String, Object> getAttributes() {
return attributes;
}
}