feat(extension): browser extension core for Chrome and Firefox#418
Conversation
- Add chrome-extension:// and moz-extension:// to CORS allowed origin patterns so the extension can make credentialed requests - Fix UserContext.getUserId() to handle JwtAuthenticationToken (set by oauth2ResourceServer for Bearer token requests) in addition to the existing UserAuthenticationToken (set by the cookie filter); the hard cast was causing a ClassCastException and 500 on all extension API calls
- Mock next/font/google in vitestSetup to fix Libre_Baskerville not-a-function error in app.test.tsx - Always render brand logo in Navbar (remove isMobile conditional that hid it on desktop); drop unused isMobile state and resize effect - Update default avatar test to assert SVG icon presence instead of stale img/src expectation
R-Sandor
left a comment
There was a problem hiding this comment.
While the sign-in seems to work for firefox, there is an issue with the auth. The bear token filter doesn't succeed when attempting to add a bookmark.
On Firefox Linux the application also doesn't persist the token (maybe?) as each time the extension is opened it requires reauthentication.
Ideally the extension should be able to maintain login in for the life of the JWT.
Please test on your end.
34e0791 to
ffb1aef
Compare
- Add jwtAuthenticationConverter inline in securityFilterChain so Bearer token requests produce UserAuthenticationToken directly; avoids registering a Converter bean that breaks Spring MVC startup - Remove JwtAuthenticationToken fallback from UserContext since the converter now handles Bearer token auth uniformly - Add accessToken to TokenRefreshResponse so the extension can read the JWT from the sign-in response body (httpOnly cookie is not accessible from moz-extension:// origin) - Build server image from source in docker-compose for local dev
| .oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder()))); | ||
| .oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder()) | ||
| .jwtAuthenticationConverter(token -> { | ||
| Number userId = token.getClaim(Constants.USER_ID_CLAIM); |
There was a problem hiding this comment.
While I think this a way that works without using a JWT filter, one thing I don't like is that it bloats the config with logic.
I think the better approach is either:
- Using a dependency injection on the JwtAuthenticationConverter with
@Bean- Assuming you then don't inherit having to implement a whole lot of things regarding mapping logic that is annoying and complex.
- Follow something like the docs here: https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html#webflux-oauth2resourceserver-jwt-authorization-extraction
Personally I am okay, with accepting it as is as this doesn't break anything and leave it to myself since you're starting to get into the meat of auth/Spring.
| public int getUserId() { | ||
| return ((UserAuthenticationToken) SecurityContextHolder.getContext().getAuthentication()) | ||
| .getUserId(); | ||
| Authentication auth = SecurityContextHolder.getContext().getAuthentication(); |
There was a problem hiding this comment.
This is much better thank you!
| this("Bearer", refreshToken, null); | ||
| public record TokenRefreshResponse(String tokenType, String accessToken, String refreshToken, String error) { | ||
| public TokenRefreshResponse(String accessToken, String refreshToken) { | ||
| this("Bearer", accessToken, refreshToken, null); |
There was a problem hiding this comment.
This makes sense, does the frotend understand this update?
| - dev | ||
| - backend | ||
| server: | ||
| build: |
R-Sandor
left a comment
There was a problem hiding this comment.
With the new changes this looks a lot better. I will merge this and address the other SecConfig cleanup.
Thanks, well done 👍 🚀
Issue number: resolves #417
Checklist
What is the current behavior?
There is no browser extension. Users must navigate to the FindFirst web app, manually enter a URL and title, and select tags to save a bookmark — interrupting their browsing flow.
The repository contained only a non-functional Firefox Manifest V2 stub and an empty Chrome directory.
What is the new behavior?
accessToken) returned in the sign-in response body and stored inbrowser.storage.local; sent asAuthorization: Beareron all API calls; session persists across popup close/open for the lifetime of the JWTsrc/) builds separate Chrome MV3 (dist/chrome/) and Firefox MV3 (dist/firefox/) artifacts via esbuildServer-side changes (this PR)
chrome-extension://*andmoz-extension://*added to allowed origin patterns so the extension can make cross-origin requestsjwtAuthenticationConverterwired intooauth2ResourceServerso Bearer token requests produceUserAuthenticationTokendirectly, makingUserContext.getUserId()work for extension API callsaccessToken(JWT) added toTokenRefreshResponsebody alongsiderefreshToken; the extension reads this from the response since thehttpOnlycookie set for web clients is not accessible from extension originsdocker-compose.ymlfor local development iterationDoes this introduce a breaking change?
Other information
Firefox manifest: Firefox MV3 uses
background.scripts(array) rather thanbackground.service_worker. The extension build produces browser-specific manifests frommanifests/manifest.firefox.jsonandmanifests/manifest.chrome.json.Docker Compose: The extension is a static build artifact loaded directly into the browser — it does not run as a container.