Skip to content
Snippets Groups Projects
Commit 074f5f8e authored by Emmanuel Bruno's avatar Emmanuel Bruno
Browse files

adds authentication.

parent 1eed12da
Branches
No related tags found
No related merge requests found
Showing
with 532 additions and 22 deletions
......@@ -105,6 +105,27 @@
<version>3.11</version>
<scope>test</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
......
......@@ -3,6 +3,9 @@ package fr.univtln.bruno.samples.jaxrs.model;
import fr.univtln.bruno.samples.jaxrs.exceptions.IllegalArgumentException;
import fr.univtln.bruno.samples.jaxrs.exceptions.NotFoundException;
import fr.univtln.bruno.samples.jaxrs.resources.PaginationInfo;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.*;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlAttribute;
......
......@@ -5,13 +5,26 @@ import fr.univtln.bruno.samples.jaxrs.exceptions.IllegalArgumentException;
import fr.univtln.bruno.samples.jaxrs.exceptions.NotFoundException;
import fr.univtln.bruno.samples.jaxrs.model.BiblioModel;
import fr.univtln.bruno.samples.jaxrs.model.BiblioModel.Auteur;
import fr.univtln.bruno.samples.jaxrs.security.BasicAuth;
import fr.univtln.bruno.samples.jaxrs.security.JWTAuth;
import fr.univtln.bruno.samples.jaxrs.security.User;
import fr.univtln.bruno.samples.jaxrs.security.UserDatabase;
import fr.univtln.bruno.samples.jaxrs.status.Status;
import io.jsonwebtoken.Jwts;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.*;
import lombok.extern.java.Log;
import javax.naming.AuthenticationException;
import java.security.SecureRandom;
import java.util.*;
import java.sql.Date;
import java.text.ParseException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.List;
@Log
// The Java class will be hosted at the URI path "/biblio"
......@@ -128,4 +141,61 @@ public class BiblioResource {
public List<Auteur> getAuteursPage(@BeanParam PaginationInfo paginationInfo) {
return modeleBibliotheque.getWithFilter(paginationInfo);
}
@GET
@Path("context")
@RolesAllowed("ADMIN")
public String getContext(@Context UriInfo uriInfo, @Context HttpHeaders httpHeaders, @Context Request request, @Context SecurityContext securityContext) throws ParseException {
return "UriInfo: (" + uriInfo.getRequestUri().toString()
+ ")\n HttpHeaders(" + httpHeaders.getRequestHeaders().toString()
//+")\n Request Precondition("+request.evaluatePreconditions(new SimpleDateFormat("dd/MM/yyyy-HH:mm:ss").parse("03/02/2021-10:30:00"))
+ ")\n SecurityContext(Auth.scheme: [" + securityContext.getAuthenticationScheme()
+ "] user: [" + securityContext.getUserPrincipal().getName()
+ "] secured: [" + securityContext.isSecure() + "] )";
}
@GET
@Path("adminsonly")
@RolesAllowed("ADMIN")
@BasicAuth
public String getRestrictedToAdmins() {
return "secret for admins !";
}
@GET
@Path("usersonly")
@RolesAllowed("USER")
@BasicAuth
public String getRestrictedToUsers() {
return "secret for users !";
}
@GET
@Path("secured")
@RolesAllowed({"USER", "ADMIN"})
@JWTAuth
public String securedByJWT(@Context SecurityContext securityContext) {
log.info("USER ACCESS :"+securityContext.getUserPrincipal().getName());
return "Access with JWT ok for "+securityContext.getUserPrincipal().getName();
}
@GET
@Path("login")
@RolesAllowed({"USER", "ADMIN"})
@BasicAuth
public String login(@Context SecurityContext securityContext) {
if (securityContext.isSecure() && securityContext.getUserPrincipal() instanceof User) {
User user = (User) securityContext.getUserPrincipal();
return Jwts.builder()
.setIssuer("sample-jaxrs")
.setIssuedAt(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
.setSubject(user.getEmail())
.claim("firstname", user.getFirstName())
.claim("lastname", user.getLastName())
.claim("roles", user.getRoles())
.setExpiration(Date.from(LocalDateTime.now().plus(15, ChronoUnit.MINUTES).atZone(ZoneId.systemDefault()).toInstant()))
.signWith(UserDatabase.KEY).compact();
}
throw new WebApplicationException(new AuthenticationException());
}
}
package fr.univtln.bruno.samples.jaxrs.security;
import jakarta.annotation.Priority;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.ext.Provider;
import lombok.SneakyThrows;
import lombok.extern.java.Log;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.*;
import java.util.stream.Collectors;
@BasicAuth
@Provider
@Priority(Priorities.AUTHENTICATION)
@Log
/**
* This class if a filter for JAX-RS to perform authentication and to check permissions against the acceded method.
*/
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String AUTHORIZATION_PROPERTY = "Authorization";
private static final String AUTHENTICATION_SCHEME = "Basic";
//We inject the data from the acceded resource.
@Context
private ResourceInfo resourceInfo;
@SneakyThrows
@Override
public void filter(ContainerRequestContext requestContext) {
//We use reflection on the acceded method to look for security annotations.
Method method = resourceInfo.getResourceMethod();
//if its PermitAll access is granted
//otherwise if its DenyAll the access is refused
if (!method.isAnnotationPresent(PermitAll.class)) {
if (method.isAnnotationPresent(DenyAll.class)) {
requestContext.abortWith(Response.status(Response.Status.FORBIDDEN)
.entity("Access denied to all users").build());
return;
}
//We get the authorization header
final String authorization = requestContext.getHeaderString(AUTHORIZATION_PROPERTY);
//We check the presence of the credentials
if (authorization == null || authorization.isEmpty()) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Please provide your credentials").build());
return;
}
//Get encoded username and password
final String encodedUserPassword = authorization.substring(AUTHENTICATION_SCHEME.length()).trim();
//Decode username and password
String usernameAndPassword = new String(Base64.getDecoder().decode(encodedUserPassword.getBytes()));
//Split username and password tokens
final StringTokenizer tokenizer = new StringTokenizer(usernameAndPassword, ":");
final String username = tokenizer.nextToken();
final String password = tokenizer.nextToken();
log.info(username + " tries to log in with " + password);
//Verify user access
if (method.isAnnotationPresent(RolesAllowed.class)) {
RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
EnumSet<UserDatabase.Role> rolesSet =
Arrays.stream(rolesAnnotation.value())
.map(r -> UserDatabase.Role.valueOf(r))
.collect(Collectors.toCollection(() -> EnumSet.noneOf(UserDatabase.Role.class)));
//We check to login/password
if (!UserDatabase.USER_DATABASE.checkPassword(username, password)) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Wrong username or password").build());
return;
}
//We check if the role is allowed
if (Collections.disjoint(rolesSet, UserDatabase.USER_DATABASE.getUserRoles(username))) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Roles not allowed").build());
return;
}
//We build a new securitycontext to transmit the security data to JAX-RS
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return UserDatabase.USER_DATABASE.getUser(username);
}
@Override
public boolean isUserInRole(String role) {
return UserDatabase.USER_DATABASE.getUserRoles(username).contains(UserDatabase.Role.valueOf(role));
}
@Override
public boolean isSecure() {
return true;
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
}
}
}
}
\ No newline at end of file
package fr.univtln.bruno.samples.jaxrs.security;
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface BasicAuth {
}
package fr.univtln.bruno.samples.jaxrs.security;
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface JWTAuth {
}
package fr.univtln.bruno.samples.jaxrs.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import jakarta.annotation.Priority;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.ext.Provider;
import lombok.SneakyThrows;
import lombok.extern.java.Log;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.stream.Collectors;
@JWTAuth
@Provider
@Priority(Priorities.AUTHENTICATION)
@Log
/**
* This class if a filter for JAX-RS to perform authentication via JWT.
*/
public class JsonWebTokenFilter implements ContainerRequestFilter {
private static final String AUTHORIZATION_PROPERTY = "Authorization";
private static final String AUTHENTICATION_SCHEME = "Bearer";
//We inject the data from the acceded resource.
@Context
private ResourceInfo resourceInfo;
@SneakyThrows
@Override
public void filter(ContainerRequestContext requestContext) {
//We use reflection on the acceded method to look for security annotations.
Method method = resourceInfo.getResourceMethod();
//if its PermitAll access is granted
//otherwise if its DenyAll the access is refused
if (!method.isAnnotationPresent(PermitAll.class)) {
if (method.isAnnotationPresent(DenyAll.class)) {
requestContext.abortWith(Response.status(Response.Status.FORBIDDEN)
.entity("Access denied to all users").build());
return;
}
//We get the authorization header
final String authorization = requestContext.getHeaderString(AUTHORIZATION_PROPERTY);
if (method.isAnnotationPresent(RolesAllowed.class)) {
RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
EnumSet<UserDatabase.Role> rolesSet =
Arrays.stream(rolesAnnotation.value())
.map(r -> UserDatabase.Role.valueOf(r))
.collect(Collectors.toCollection(() -> EnumSet.noneOf(UserDatabase.Role.class)));
//We check the credentials presence
if (authorization == null || authorization.isEmpty()) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Please provide your credentials").build());
return;
}
//Gets the token
log.info("AUTH: "+authorization);
final String compactJwt = authorization.substring(AUTHENTICATION_SCHEME.length()).trim();
if (!authorization.contains(AUTHENTICATION_SCHEME) || compactJwt == null || compactJwt.isEmpty()) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Please provide your credentials").build());
return;
}
log.info("JWT: "+compactJwt);
Jws<Claims> jws = Jwts.parserBuilder()
.setSigningKey(UserDatabase.KEY)
.build()
.parseClaimsJws(compactJwt);
log.info("JWT decoded: "+jws.toString());
final String username = jws.getBody().getSubject();
//We check if the role is allowed
if (Collections.disjoint(rolesSet, UserDatabase.USER_DATABASE.getUserRoles(username))) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Roles not allowed").build());
return;
}
//We build a new securitycontext to transmit the security data to JAX-RS
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return UserDatabase.USER_DATABASE.getUser(username);
}
@Override
public boolean isUserInRole(String role) {
return UserDatabase.USER_DATABASE.getUserRoles(username).contains(UserDatabase.Role.valueOf(role));
}
@Override
public boolean isSecure() {
return true;
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
}
}
}
}
\ No newline at end of file
package fr.univtln.bruno.samples.jaxrs.security;
import lombok.*;
import lombok.experimental.Delegate;
import lombok.experimental.FieldDefaults;
import lombok.extern.java.Log;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.*;
@Log
@FieldDefaults(level = AccessLevel.PRIVATE)
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(of = "email")
public class User implements Principal {
UUID uuid = UUID.randomUUID();
String firstName, lastName, email;
byte[] passwordHash;
byte[] salt = new byte[16];
@Delegate
EnumSet<UserDatabase.Role> roles;
SecureRandom random = new SecureRandom();
@Builder
public User(String firstName, String lastName, String email, String password, EnumSet<UserDatabase.Role> roles)
throws NoSuchAlgorithmException, InvalidKeySpecException {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.roles = roles;
random.nextBytes(salt);
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 128);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
passwordHash = factory.generateSecret(spec).getEncoded();
}
@Override
public String getName() {
return lastName + ", " + firstName+" <"+email+">";
}
public String toString() {
return email + "" + Base64.getEncoder().encodeToString(passwordHash);
}
public boolean checkPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 128);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] submittedPasswordHash = factory.generateSecret(spec).getEncoded();
return Arrays.equals(passwordHash, submittedPasswordHash);
}
}
\ No newline at end of file
package fr.univtln.bruno.samples.jaxrs.security;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.ToString;
import lombok.extern.java.Log;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
@Log
@ToString
public class UserDatabase {
public static final UserDatabase USER_DATABASE = new UserDatabase();
// We need a signing key for the id token, so we'll create one just for this example. Usually
// the key would be read from your application configuration instead.
public static final Key KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
static {
try {
USER_DATABASE.addUser("John", "Doe", "john.doe@nowhere.com", "admin", EnumSet.of(Role.ADMIN));
USER_DATABASE.addUser("William", "Smith", "william.smith@here.net", "user", EnumSet.of(Role.USER));
USER_DATABASE.addUser("Mary", "Robert", "mary.roberts@here.net", "user", EnumSet.of(Role.USER));
} catch (InvalidKeySpecException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
private final Map<String, User> users = new HashMap<>();
public static void main(String[] args) {
USER_DATABASE.users.values().forEach(u->log.info(u.toString()));
}
public void addUser(String firstname, String lastname, String email, String password, EnumSet<Role> roles)
throws InvalidKeySpecException, NoSuchAlgorithmException {
users.put(email, User.builder().firstName(firstname).lastName(lastname).email(email).password(password).roles(roles).build());
}
public Map<String, User> getUsers() {
return Collections.unmodifiableMap(users);
}
public void removeUser(String email) {
users.remove(email);
}
public boolean checkPassword(String email, String password) throws InvalidKeySpecException, NoSuchAlgorithmException {
return users.get(email).checkPassword(password);
}
public User getUser(String email) {
return users.get(email);
}
public Set<Role> getUserRoles(String email) {
return users.get(email).getRoles();
}
public enum Role {ADMIN, USER, GUEST}
}
......@@ -18,25 +18,6 @@ public class BiblioServer {
// Base URI the Grizzly HTTP server will listen on
public static final String BASE_URI = "http://0.0.0.0:9998/myapp";
/**
* Starts Grizzly HTTP server exposing JAX-RS resources defined in this application.
*
* @return Grizzly HTTP server.
*/
public static HttpServer startServer() {
// create a resource config that scans for JAX-RS resources and providers
// in demos package and add a logging feature to the server.
Logger logger = Logger.getLogger(BiblioServer.class.getName());
final ResourceConfig rc = new ResourceConfig()
.packages(true, "fr.univtln.bruno.samples.jaxrs")
.register(new LoggingFeature(logger, Level.INFO, null, null));
// create and start a new instance of grizzly http server
// exposing the Jersey application at BASE_URI
return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
}
/**
* Main method.
*
......@@ -56,4 +37,24 @@ public class BiblioServer {
Thread.currentThread().join();
server.shutdown();
}
/**
* Starts Grizzly HTTP server exposing JAX-RS resources defined in this application.
*
* @return Grizzly HTTP server.
*/
public static HttpServer startServer() {
// create a resource config that scans for JAX-RS resources and providers
// in demos package and add a logging feature to the server.
Logger logger = Logger.getLogger(BiblioServer.class.getName());
logger.setLevel(Level.FINE);
final ResourceConfig rc = new ResourceConfig()
.packages(true, "fr.univtln.bruno.samples.jaxrs")
.register(new LoggingFeature(logger, Level.INFO, LoggingFeature.Verbosity.PAYLOAD_TEXT, null));
// create and start a new instance of grizzly http server
// exposing the Jersey application at BASE_URI
return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment