-
Notifications
You must be signed in to change notification settings - Fork 2
Feat/spring security setup #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
8db8572
feat: add OAuth2 resource server dependency in pom.xml
annikaholmqvist94 90517ef
feat: add JWT configuration properties in application.properties
annikaholmqvist94 c8c05ee
feat: add JwtProperties to load JWT configuration from application.pr…
annikaholmqvist94 56c6df5
feat: add JwtService for handling JWT creation and validation
annikaholmqvist94 3a6a56b
feat: add JwtAuthenticationFilter for token-based request authentication
annikaholmqvist94 74ca7b6
feat: configure SecurityConfig for JWT and CORS setup
annikaholmqvist94 8328c26
feat: implement CustomUserDetailsService for user authentication inte…
annikaholmqvist94 d606938
CustomUserDetailsService for user authentication integration
annikaholmqvist94 85928ae
test: add JwtService and CustomUserDetailsService mocks in VetControl…
annikaholmqvist94 201c64a
test: add JwtService and CustomUserDetailsService mocks in ActivityLo…
annikaholmqvist94 67000d1
test: add JwtService and CustomUserDetailsService mocks in ClinicCont…
annikaholmqvist94 295a52a
test: add JwtService and CustomUserDetailsService mocks in CommentCon…
annikaholmqvist94 aaee79b
test: add JwtService and CustomUserDetailsService mocks in MedicalRec…
annikaholmqvist94 bdd6c7e
Merge remote-tracking branch 'origin/main' into feat/spring-security-…
annikaholmqvist94 07bce62
test: add authentication setup in ActivityLogIntegrationTest and Atta…
annikaholmqvist94 289fbd1
fix: specify UTF-8 charset for secret key generation in JwtService
annikaholmqvist94 184fbbe
fix: handle UsernameNotFoundException in JwtAuthenticationFilter
annikaholmqvist94 bc1a131
fix: validate secret key length for HS256 in JwtService
annikaholmqvist94 1d19d75
refactor: replace @RequestHeader with @AuthenticationPrincipal in Pet…
annikaholmqvist94 a7d1f66
refactor: use @AuthenticationPrincipal in ActivityLogController
annikaholmqvist94 c7ae70e
test: update ActivityLogControllerTest for Spring Security setup
annikaholmqvist94 8e1f665
test: remove redundant userId headers in ActivityLogIntegrationTest
annikaholmqvist94 7865e17
docs: add comprehensive API usage guide for frontend developers
annikaholmqvist94 965dcf1
test: clean up comments and update application-test.properties
annikaholmqvist94 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/main/java/org/example/vet1177/security/CustomUserDetailsService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package org.example.vet1177.security; | ||
|
|
||
| import org.example.vet1177.repository.UserRepository; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.security.core.userdetails.UserDetails; | ||
| import org.springframework.security.core.userdetails.UserDetailsService; | ||
| import org.springframework.security.core.userdetails.UsernameNotFoundException; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| /** | ||
| * Bryggan mellan Spring Security och vår databas. | ||
| * | ||
| * Spring Security behöver veta hur man hämtar en användare givet ett "användarnamn". | ||
| * I vårt fall är "användarnamnet" email-adressen (det som står i JWT:ns subject-claim). | ||
| * | ||
| * Flödet: | ||
| * JWT-token innehåller sub="sara@vet.se" | ||
| * ↓ | ||
| * JwtAuthenticationFilter anropar loadUserByUsername("sara@vet.se") | ||
| * ↓ | ||
| * Vi söker i databasen: userRepository.findByEmail("sara@vet.se") | ||
| * ↓ | ||
| * Returnerar User-objektet (som implementerar UserDetails) | ||
| * ↓ | ||
| * Spring Security sätter det i SecurityContext → @AuthenticationPrincipal fungerar | ||
| */ | ||
| @Service | ||
| public class CustomUserDetailsService implements UserDetailsService { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class); | ||
|
|
||
| private final UserRepository userRepository; | ||
|
|
||
| public CustomUserDetailsService(UserRepository userRepository) { | ||
| this.userRepository = userRepository; | ||
| } | ||
|
|
||
| @Override | ||
| public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { | ||
| log.debug("Loading user by email={}", email); | ||
|
|
||
|
|
||
| return userRepository.findByEmail(email) | ||
| .orElseThrow(() -> { | ||
| log.warn("User not found email={}", email); | ||
| return new UsernameNotFoundException("Användare hittades inte: " + email); | ||
| }); | ||
| } | ||
| } | ||
128 changes: 128 additions & 0 deletions
128
src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| package org.example.vet1177.security; | ||
|
|
||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.security.oauth2.jwt.Jwt; | ||
| import org.springframework.security.core.userdetails.UsernameNotFoundException; | ||
| import org.springframework.security.oauth2.jwt.JwtException; | ||
| import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| /** | ||
| * Filter som körs EN gång per HTTP-request (OncePerRequestFilter). | ||
| * | ||
| * Vad händer steg för steg: | ||
| * | ||
| * [Klient] → HTTP Request med "Authorization: Bearer eyJhbG..." | ||
| * ↓ | ||
| * [JwtAuthenticationFilter] | ||
| * 1. Läser Authorization-headern | ||
| * 2. Extraherar token-strängen (allt efter "Bearer ") | ||
| * 3. Avkodar token via JwtService → får email + roll | ||
| * 4. Laddar User från DB via CustomUserDetailsService | ||
| * 5. Skapar ett Authentication-objekt och sätter det i SecurityContext | ||
| * ↓ | ||
| * [Spring Security] | ||
| * Kollar SecurityContext: finns en autentiserad användare? | ||
| * Om ja → kontrollerar att användaren har rätt roll för endpointen | ||
| * Om nej → returnerar 401 Unauthorized | ||
| * ↓ | ||
| * [Controller] | ||
| * @AuthenticationPrincipal User currentUser ← hämtas från SecurityContext | ||
| */ | ||
| @Component | ||
| public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class); | ||
|
|
||
| private final JwtService jwtService; | ||
| private final CustomUserDetailsService userDetailsService; | ||
|
|
||
| public JwtAuthenticationFilter(JwtService jwtService, CustomUserDetailsService userDetailsService) { | ||
| this.jwtService = jwtService; | ||
| this.userDetailsService = userDetailsService; | ||
| } | ||
|
|
||
| @Override | ||
| protected void doFilterInternal( | ||
| HttpServletRequest request, | ||
| HttpServletResponse response, | ||
| FilterChain filterChain) throws ServletException, IOException { | ||
|
|
||
| // 1. Läs Authorization-headern | ||
| // Formatet är: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | ||
| String authHeader = request.getHeader("Authorization"); | ||
|
|
||
| // Om headern saknas eller inte börjar med "Bearer " → skippa filtret. | ||
| // Requesten släpps vidare i kedjan. Om endpointen kräver auth | ||
| // kommer Spring Security att returnera 401 automatiskt. | ||
| if (authHeader == null || !authHeader.startsWith("Bearer ")) { | ||
| filterChain.doFilter(request, response); | ||
| return; | ||
| } | ||
|
|
||
| // 2. Extrahera token-strängen (ta bort "Bearer " som är 7 tecken) | ||
| String token = authHeader.substring(7); | ||
|
|
||
| try { | ||
| // 3. Avkoda och validera token. | ||
| // Om signaturen inte stämmer eller token har gått ut kastar decode() JwtException. | ||
| Jwt jwt = jwtService.decode(token); | ||
|
|
||
| // 4. Hämta email från token (subject-claim) | ||
| String email = jwt.getSubject(); | ||
|
|
||
| // Kolla att vi inte redan har autentiserat denna request | ||
| // (kan hända om flera filter kör i kedjan) | ||
| if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { | ||
|
|
||
| // 5. Ladda det fulla User-objektet från databasen. | ||
| // Vi behöver hela entiteten (med id, klinik, roll) för @AuthenticationPrincipal. | ||
| var userDetails = userDetailsService.loadUserByUsername(email); | ||
|
|
||
| // 6. Skapa ett Authentication-objekt. | ||
| // UsernamePasswordAuthenticationToken är Springs standardklass för "en autentiserad användare". | ||
| // - Första argumentet (principal) → User-objektet (det som @AuthenticationPrincipal ger) | ||
| // - Andra argumentet (credentials) → null (vi behöver inte lösenordet, token räcker) | ||
| // - Tredje argumentet (authorities) → rollerna (ROLE_VET, ROLE_ADMIN, etc.) | ||
| var authentication = new UsernamePasswordAuthenticationToken( | ||
| userDetails, null, userDetails.getAuthorities() | ||
| ); | ||
|
|
||
| // Kopplar request-detaljer (IP-adress, session-id) till autentiseringen — för loggning. | ||
| authentication.setDetails( | ||
| new WebAuthenticationDetailsSource().buildDetails(request) | ||
| ); | ||
|
|
||
| // 7. Sätt autentiseringen i SecurityContext. | ||
| // Från och med nu "vet" Spring Security att det finns en inloggad användare. | ||
| // Alla controllers kan hämta den via @AuthenticationPrincipal. | ||
| SecurityContextHolder.getContext().setAuthentication(authentication); | ||
|
|
||
| log.debug("Authenticated user email={} via JWT", email); | ||
| } | ||
|
|
||
| } catch (JwtException e) { | ||
| // Token är ogiltig (felaktig signatur, utgången, korrupt). | ||
| // Vi loggar och släpper vidare — Spring Security returnerar 401 automatiskt | ||
| // eftersom SecurityContext förblir tom. | ||
| log.warn("Invalid JWT token: {}", e.getMessage()); | ||
| } catch (UsernameNotFoundException e) { | ||
| // Token är giltig men användaren finns inte längre i databasen | ||
| // (t.ex. raderad av admin medan tokenen fortfarande gäller). | ||
| log.warn("User from JWT no longer exists: {}", e.getMessage()); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Släpp vidare requesten till nästa filter i kedjan (och sedan till controllern) | ||
| filterChain.doFilter(request, response); | ||
| } | ||
| } | ||
22 changes: 22 additions & 0 deletions
22
src/main/java/org/example/vet1177/security/JwtProperties.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package org.example.vet1177.security; | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| /** | ||
| * Läser JWT-konfiguration från application.properties. | ||
| * | ||
| * jwt.secret-key → den hemliga nyckeln som används för att signera/verifiera tokens (HMAC SHA-256) | ||
| * jwt.expiration-ms → hur länge en token är giltig i millisekunder (86400000 = 24 timmar) | ||
| */ | ||
| @ConfigurationProperties(prefix = "jwt") | ||
| public class JwtProperties { | ||
|
|
||
| private String secretKey; | ||
| private long expirationMs; | ||
|
|
||
| public String getSecretKey() { return secretKey; } | ||
| public void setSecretKey(String secretKey) { this.secretKey = secretKey; } | ||
|
|
||
| public long getExpirationMs() { return expirationMs; } | ||
| public void setExpirationMs(long expirationMs) { this.expirationMs = expirationMs; } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid logging email addresses in production logs.
Logging
emailin the warning message may violate privacy requirements (GDPR/CCPA) as emails are PII. Consider logging a hash or partial email, or removing the email from the log entirely.🛡️ Suggested fix
return userRepository.findByEmail(email) .orElseThrow(() -> { - log.warn("User not found email={}", email); + log.warn("User not found for provided email"); return new UsernameNotFoundException("Användare hittades inte: " + email); });📝 Committable suggestion
🤖 Prompt for AI Agents