feat: add JWT authentication primitives
This commit is contained in:
1 parent
fcbef8490f
commit
1f09418d7b
4 files changed
+152
No files matched your search
@@ -0,0 +1,6 @@
|
||||
package com.slotline.security;
|
||||
|
||||
import com.slotline.user.Role;
|
||||
|
||||
public record AuthenticatedUser(Long id, String username, Role role, Long organizationId) {
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.slotline.security;
|
||||
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final JwtService jwtService;
|
||||
|
||||
public JwtAuthenticationFilter(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
boolean bearer = header != null && header.startsWith(BEARER_PREFIX);
|
||||
if (bearer && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
try {
|
||||
AuthenticatedUser principal = jwtService.parse(header.substring(BEARER_PREFIX.length()));
|
||||
List<SimpleGrantedAuthority> authorities =
|
||||
List.of(new SimpleGrantedAuthority("ROLE_" + principal.role().name()));
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(principal, null, authorities);
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} catch (JwtException invalidToken) {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.slotline.security;
|
||||
|
||||
import com.slotline.config.SlotlineProperties;
|
||||
import com.slotline.user.Role;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
|
||||
private final SecretKey signingKey;
|
||||
private final Duration expiration;
|
||||
private final String issuer;
|
||||
|
||||
public JwtService(SlotlineProperties properties) {
|
||||
this.signingKey = Keys.hmacShaKeyFor(properties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8));
|
||||
this.expiration = properties.getJwt().getExpiration();
|
||||
this.issuer = properties.getJwt().getIssuer();
|
||||
}
|
||||
|
||||
public String issue(Long userId, String username, Role role, Long organizationId) {
|
||||
Instant now = Instant.now();
|
||||
return Jwts.builder()
|
||||
.issuer(issuer)
|
||||
.subject(username)
|
||||
.claim("uid", userId)
|
||||
.claim("role", role.name())
|
||||
.claim("org", organizationId)
|
||||
.issuedAt(Date.from(now))
|
||||
.expiration(Date.from(now.plus(expiration)))
|
||||
.signWith(signingKey)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public AuthenticatedUser parse(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(signingKey)
|
||||
.requireIssuer(issuer)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
Long userId = ((Number) claims.get("uid")).longValue();
|
||||
Role role = Role.valueOf(claims.get("role", String.class));
|
||||
Long organizationId = ((Number) claims.get("org")).longValue();
|
||||
return new AuthenticatedUser(userId, claims.getSubject(), role, organizationId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.slotline.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class RestAuthenticationHandlers {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public RestAuthenticationHandlers(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public AuthenticationEntryPoint authenticationEntryPoint() {
|
||||
return (request, response, exception) ->
|
||||
write(response, HttpStatus.UNAUTHORIZED, "Authentication required", request.getRequestURI());
|
||||
}
|
||||
|
||||
public AccessDeniedHandler accessDeniedHandler() {
|
||||
return (request, response, exception) ->
|
||||
write(response, HttpStatus.FORBIDDEN, "Access denied", request.getRequestURI());
|
||||
}
|
||||
|
||||
private void write(HttpServletResponse response, HttpStatus status, String detail, String path) throws IOException {
|
||||
ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, detail);
|
||||
problem.setTitle(status.getReasonPhrase());
|
||||
problem.setProperty("path", path);
|
||||
response.setStatus(status.value());
|
||||
response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
|
||||
objectMapper.writeValue(response.getOutputStream(), problem);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user