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