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

adds authentication and samples.

parent 074f5f8e
Branches
No related tags found
No related merge requests found
Showing
with 612 additions and 85 deletions
......@@ -40,12 +40,12 @@ curl -s -D - -H "Accept: application/json" \
Removes an author
```shell
curl -s -D - -X DELETE "http://localhost:9998/myapp/biblio/authors/1"
curl -s -D - -X DELETE "http://localhost:9998/myapp/biblio/auteurs/1"
```
Removes all authors
```shell
curl -s -D - -X DELETE "http://localhost:9998/myapp/biblio/authors"
curl -s -D - -X DELETE "http://localhost:9998/myapp/biblio/auteurs"
```
Adds an author
......@@ -77,8 +77,13 @@ Filter resources with query parameters :
curl -v -H "Accept: application/json" \
"http://127.0.0.1:9998/myapp/biblio/auteurs/filter?nom=Durand&prenom⁼Marie"
```
Control sort key with header param (default value "nom") :
```shell
curl -v -H "Accept: application/json" -H "sortKey: prenom"\
"http://127.0.0.1:9998/myapp/biblio/auteurs/filter"
```
Login and get a Java Web Token
```shell
TOKEN=$(curl -v --user "john.doe@nowhere.com:admin" "http://localhost:9998/myapp/biblio/login")
```
\ No newline at end of file
# curl -D - http://localhost:9998/myapp/biblio
### Get a Hello message
GET http://localhost:9998/myapp/biblio
### Init the database with two authors
PUT http://localhost:9998/myapp/biblio/init
### Get author 1 in JSON
GET http://localhost:9998/myapp/biblio/auteurs/1
Accept: application/json
### Get author 2 in XML
GET http://localhost:9998/myapp/biblio/auteurs/2
Accept: text/xml
### Get authors in JSON
GET http://localhost:9998/myapp/biblio/auteurs
Accept: application/json
### Removes an author
DELETE http://localhost:9998/myapp/biblio/auteurs/1
### Removes all authors
DELETE http://localhost:9998/myapp/biblio/auteurs
### Adds an author
POST http://localhost:9998/myapp/biblio/auteurs/
Accept: application/json
Content-type: application/json
{"nom":"John","prenom":"Smith","biographie":"My life"}
### Fully update an author
PUT http://localhost:9998/myapp/biblio/auteurs/1
Accept: application/json
Content-type: application/json
{"nom":"Martin","prenom":"Jean","biographie":"ma vie"}
### If a resource doesn't exist an exception is raised, and the 404 http status code is returned
GET http://localhost:9998/myapp/biblio/auteurs/1000
Accept: application/json
### Filter resources with query parameters :
GET http://localhost:9998/myapp/biblio/auteurs/filter?nom=Durand&prenom⁼Marie
Accept: application/json
### Control sort key with header param (default value "nom") :
GET http://127.0.0.1:9998/myapp/biblio/auteurs/filter
Accept: application/json
sortKey: prenom
### Init the database with 10k random authors
PUT http://localhost:9998/myapp/biblio/init/10000
### Get page 3 with page size of 10 authors sorted by lastname
GET http://localhost:9998/myapp/biblio/auteurs/page?pageSize=10&page=3
Accept: application/json
sortKey: prenom
### Authorization by token, part 1. Retrieve and save token with Basic Authentication
# TOKEN=$(curl -v --user "john.doe@nowhere.com:admin" "http://localhost:9998/myapp/biblio/login")
GET http://localhost:9998/myapp/biblio/login
Authorization: Basic john.doe@nowhere.com admin
> {% client.global.set("auth_token", response.body); %}
### Authorization by token, part 2. Use token to authorize. Admin & User OK
# curl -H "Authorization: Bearer $TOKEN" -v "http://localhost:9998/myapp/biblio/secured"
GET http://localhost:9998/myapp/biblio/secured
Authorization: Bearer {{auth_token}}
### Authorization by token, part 2. Use token to authorize. Admin OK
GET http://localhost:9998/myapp/biblio/secured/admin
Authorization: Bearer {{auth_token}}
### Authorization with another user.
# TOKEN=$(curl -v --user "mary.roberts@here.net:user" "http://localhost:9998/myapp/biblio/login")
GET http://localhost:9998/myapp/biblio/login
Authorization: Basic mary.roberts@here.net user
> {% client.global.set("auth_token", response.body); %}
### Authorization by token, part 2. Use token to authorize. Admin & User OK.
# curl -H "Authorization: Bearer $TOKEN" -v "http://localhost:9998/myapp/biblio/secured"
GET http://localhost:9998/myapp/biblio/secured
Authorization: Bearer {{auth_token}}
### Authorization by token, part 2. Use token to authorize. Admin KO.
GET http://localhost:9998/myapp/biblio/secured/admin
Authorization: Bearer {{auth_token}}
\ No newline at end of file
......@@ -3,9 +3,6 @@ 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;
......@@ -27,16 +24,29 @@ import java.util.stream.Stream;
import static fr.univtln.bruno.samples.jaxrs.model.BiblioModel.Field.valueOf;
/**
* The type Biblio model.
*/
@Log
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(staticName = "of")
public class BiblioModel {
private static AtomicLong lastId = new AtomicLong(0);
public enum Field {NOM, PRENOM, BIOGRAPHIE}
private static final AtomicLong lastId = new AtomicLong(0);
@Delegate
final MutableLongObjectMap<Auteur> auteurs = LongObjectMaps.mutable.empty();
/**
* Add auteur auteur.
*
* @param auteur the auteur
* @return the auteur
* @throws IllegalArgumentException the illegal argument exception
*/
public Auteur addAuteur(Auteur auteur) throws IllegalArgumentException {
if (auteur.id != 0) throw new IllegalArgumentException();
auteur.id = lastId.incrementAndGet();
......@@ -44,6 +54,15 @@ public class BiblioModel {
return auteur;
}
/**
* Update auteur auteur.
*
* @param id the id
* @param auteur the auteur
* @return the auteur
* @throws NotFoundException the not found exception
* @throws IllegalArgumentException the illegal argument exception
*/
public Auteur updateAuteur(long id, Auteur auteur) throws NotFoundException, IllegalArgumentException {
if (auteur.id != 0) throw new IllegalArgumentException();
auteur.id = id;
......@@ -52,33 +71,63 @@ public class BiblioModel {
return auteur;
}
/**
* Remove auteur.
*
* @param id the id
* @throws NotFoundException the not found exception
*/
public void removeAuteur(long id) throws NotFoundException {
if (!auteurs.containsKey(id)) throw new NotFoundException();
auteurs.remove(id);
}
/**
* Gets auteur.
*
* @param id the id
* @return the auteur
* @throws NotFoundException the not found exception
*/
public Auteur getAuteur(long id) throws NotFoundException {
if (!auteurs.containsKey(id)) throw new NotFoundException();
return auteurs.get(id);
}
/**
* Gets auteur size.
*
* @return the auteur size
*/
public int getAuteurSize() {
return auteurs.size();
}
/**
* Returns a sorted, filtered and paginated list of authors.
*
* @param paginationInfo the pagination info
* @return the sorted, filtered page.
*/
public List<Auteur> getWithFilter(PaginationInfo paginationInfo) {
//We build a author stream, first we add sorting
Stream<Auteur> auteurStream = auteurs.stream()
.sorted(Comparator.comparing(auteur -> switch (valueOf(paginationInfo.getSortKey().toUpperCase())) {
case NOM -> auteur.getNom();
case PRENOM -> auteur.getPrenom();
default -> throw new InvalidParameterException();
}));
//The add filters according to parameters
if (paginationInfo.getNom()!=null)
auteurStream = auteurStream.filter(auteur -> auteur.getNom().equalsIgnoreCase(paginationInfo.getNom()));
if (paginationInfo.getPrenom()!=null)
auteurStream = auteurStream.filter(auteur -> auteur.getPrenom().equalsIgnoreCase(paginationInfo.getPrenom()));
if (paginationInfo.getBiographie()!=null)
auteurStream = auteurStream.filter(auteur -> auteur.getBiographie().contains(paginationInfo.getBiographie()));
//Finally add pagination instructions.
if ((paginationInfo.getPage() > 0) && (paginationInfo.getPageSize() > 0)) {
auteurStream = auteurStream
.skip(paginationInfo.getPageSize() * (paginationInfo.getPage() - 1))
......@@ -93,8 +142,9 @@ public class BiblioModel {
lastId.set(0);
}
public enum Field {NOM, PRENOM, BIOGRAPHIE}
/**
* The type Auteur.
*/
@Builder
@Getter
@Setter
......
......@@ -5,10 +5,10 @@ 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.annotations.BasicAuth;
import fr.univtln.bruno.samples.jaxrs.security.annotations.JWTAuth;
import fr.univtln.bruno.samples.jaxrs.security.User;
import fr.univtln.bruno.samples.jaxrs.security.UserDatabase;
import fr.univtln.bruno.samples.jaxrs.security.InMemoryLoginModule;
import fr.univtln.bruno.samples.jaxrs.status.Status;
import io.jsonwebtoken.Jwts;
import jakarta.annotation.security.RolesAllowed;
......@@ -18,12 +18,11 @@ import lombok.extern.java.Log;
import javax.naming.AuthenticationException;
import java.security.SecureRandom;
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.Date;
import java.util.List;
@Log
......@@ -86,7 +85,7 @@ public class BiblioResource {
*
* @param auteur The author to be added without its id.
* @return The added author with its id.
* @throws IllegalArgumentException
* @throws IllegalArgumentException if the author has an explicit id (id!=0).
*/
@POST
@Status(Status.CREATED)
......@@ -145,10 +144,9 @@ public class BiblioResource {
@GET
@Path("context")
@RolesAllowed("ADMIN")
public String getContext(@Context UriInfo uriInfo, @Context HttpHeaders httpHeaders, @Context Request request, @Context SecurityContext securityContext) throws ParseException {
public String getContext(@Context UriInfo uriInfo, @Context HttpHeaders httpHeaders, @Context Request request, @Context SecurityContext securityContext) {
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() + "] )";
......@@ -158,31 +156,43 @@ public class BiblioResource {
@Path("adminsonly")
@RolesAllowed("ADMIN")
@BasicAuth
public String getRestrictedToAdmins() {
return "secret for admins !";
public String getRestrictedToAdmins(@Context SecurityContext securityContext) {
return "secret for admins !" + securityContext.getUserPrincipal().getName();
}
@GET
@Path("usersonly")
@RolesAllowed("USER")
@BasicAuth
public String getRestrictedToUsers() {
return "secret for users !";
public String getRestrictedToUsers(@Context SecurityContext securityContext) {
return "secret for users ! to " + securityContext.getUserPrincipal().getName();
}
@GET
@Path("secured")
@RolesAllowed({"USER", "ADMIN"})
@JWTAuth
@Produces({MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
public String securedByJWT(@Context SecurityContext securityContext) {
log.info("USER ACCESS :" + securityContext.getUserPrincipal().getName());
return "Access with JWT ok for " + securityContext.getUserPrincipal().getName();
}
@GET
@Path("secured/admin")
@RolesAllowed({"ADMIN"})
@JWTAuth
@Produces({MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
public String securedByJWTAdminOnly(@Context SecurityContext securityContext) {
log.info("ADMIN ACCESS :" + securityContext.getUserPrincipal().getName());
return "Access with JWT ok for " + securityContext.getUserPrincipal().getName();
}
@GET
@Path("login")
@RolesAllowed({"USER", "ADMIN"})
@BasicAuth
@Produces({MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
public String login(@Context SecurityContext securityContext) {
if (securityContext.isSecure() && securityContext.getUserPrincipal() instanceof User) {
User user = (User) securityContext.getUserPrincipal();
......@@ -194,7 +204,7 @@ public class BiblioResource {
.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();
.signWith(InMemoryLoginModule.KEY).compact();
}
throw new WebApplicationException(new AuthenticationException());
}
......
......@@ -13,18 +13,20 @@ import lombok.experimental.FieldDefaults;
@NoArgsConstructor
@AllArgsConstructor
public class PaginationInfo {
@HeaderParam("sortKey")
@DefaultValue("nom")
String sortKey;
@SuppressWarnings("FieldMayBeFinal")
@QueryParam("page")
@Builder.Default
long page = 1;
@SuppressWarnings("FieldMayBeFinal")
@QueryParam("pageSize")
@Builder.Default
long pageSize = 10;
@HeaderParam("sortKey")
@DefaultValue("nom")
String sortKey;
@QueryParam("nom")
String nom;
......
......@@ -2,7 +2,9 @@ package fr.univtln.bruno.samples.jaxrs.security;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.AccessLevel;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
import lombok.extern.java.Log;
import java.security.Key;
......@@ -10,57 +12,126 @@ import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
/**
* The type User database.
*/
@Log
@ToString
public class UserDatabase {
public static final UserDatabase USER_DATABASE = new UserDatabase();
@FieldDefaults(level = AccessLevel.PRIVATE)
public class InMemoryLoginModule {
/**
* The constant USER_DATABASE mocks a user database in memory.
*/
public static final InMemoryLoginModule USER_DATABASE = new InMemoryLoginModule();
// 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.
/**
* The constant KEY is used as a signing key for the bearer JWT token.
* It is used to check that the token hasn't been modified.
*/
public static final Key KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
//We add three demo users.
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();
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
log.severe("In memory user database error "+e.getLocalizedMessage());
}
}
private final Map<String, User> users = new HashMap<>();
final Map<String, User> users = new HashMap<>();
public static void main(String[] args) {
USER_DATABASE.users.values().forEach(u->log.info(u.toString()));
public static boolean isInRoles(Set<Role> rolesSet, String username) {
return !(Collections.disjoint(rolesSet, InMemoryLoginModule.USER_DATABASE.getUserRoles(username)));
}
public void addUser(String firstname, String lastname, String email, String password, EnumSet<Role> roles)
/**
* Add user.
*
* @param firstname the firstname
* @param lastname the lastname
* @param email the email
* @param password the password
* @param roles the roles
* @throws InvalidKeySpecException the invalid key spec exception
* @throws NoSuchAlgorithmException the no such algorithm exception
*/
public void addUser(String firstname, String lastname, String email, String password, Set<Role> roles)
throws InvalidKeySpecException, NoSuchAlgorithmException {
users.put(email, User.builder().firstName(firstname).lastName(lastname).email(email).password(password).roles(roles).build());
}
/**
* Gets users.
*
* @return the users
*/
public Map<String, User> getUsers() {
return Collections.unmodifiableMap(users);
}
/**
* Remove user.
*
* @param email the email
*/
public void removeUser(String email) {
users.remove(email);
}
public boolean checkPassword(String email, String password) throws InvalidKeySpecException, NoSuchAlgorithmException {
/**
* Check password boolean.
*
* @param email the email
* @param password the password
* @return the boolean
*/
public boolean login(String email, String password) {
return users.get(email).checkPassword(password);
}
/**
* Gets user.
*
* @param email the email
* @return the user
*/
public User getUser(String email) {
return users.get(email);
}
/**
* Gets user roles.
*
* @param email the email
* @return the user roles
*/
public Set<Role> getUserRoles(String email) {
return users.get(email).getRoles();
}
public enum Role {ADMIN, USER, GUEST}
@SuppressWarnings("SameReturnValue")
public boolean logout() {
return false;
}
/**
* The enum Role.
*/
public enum Role {
/**
* Admin role.
*/
ADMIN,
/**
* User role.
*/
USER,
/**
* Guest role.
*/
GUEST
}
}
package fr.univtln.bruno.samples.jaxrs.security;
import jakarta.ws.rs.core.SecurityContext;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.security.Principal;
@FieldDefaults(level = AccessLevel.PRIVATE)
@AllArgsConstructor(staticName = "newInstance")
public class MySecurityContext implements SecurityContext {
private final String authenticationScheme;
private final String username;
@Override
public Principal getUserPrincipal() {
return InMemoryLoginModule.USER_DATABASE.getUser(username);
}
@Override
public boolean isUserInRole(String role) {
return InMemoryLoginModule.USER_DATABASE.getUserRoles(username).contains(InMemoryLoginModule.Role.valueOf(role));
}
@Override
public boolean isSecure() {
return true;
}
@Override
public String getAuthenticationScheme() {
return authenticationScheme;
}
}
......@@ -23,17 +23,19 @@ import java.util.*;
@EqualsAndHashCode(of = "email")
public class User implements Principal {
UUID uuid = UUID.randomUUID();
String firstName, lastName, email;
String firstName;
String lastName;
String email;
byte[] passwordHash;
byte[] salt = new byte[16];
@Delegate
EnumSet<UserDatabase.Role> roles;
Set<InMemoryLoginModule.Role> roles;
SecureRandom random = new SecureRandom();
@Builder
public User(String firstName, String lastName, String email, String password, EnumSet<UserDatabase.Role> roles)
public User(String firstName, String lastName, String email, String password, Set<InMemoryLoginModule.Role> roles)
throws NoSuchAlgorithmException, InvalidKeySpecException {
this.firstName = firstName;
this.lastName = lastName;
......@@ -55,7 +57,8 @@ public class User implements Principal {
return email + "" + Base64.getEncoder().encodeToString(passwordHash);
}
public boolean checkPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
@SneakyThrows
public boolean checkPassword(String password) {
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 128);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] submittedPasswordHash = factory.generateSecret(spec).getEncoded();
......
package fr.univtln.bruno.samples.jaxrs.security;
package fr.univtln.bruno.samples.jaxrs.security.annotations;
import jakarta.ws.rs.NameBinding;
......
package fr.univtln.bruno.samples.jaxrs.security;
package fr.univtln.bruno.samples.jaxrs.security.annotations;
import jakarta.ws.rs.NameBinding;
......
package fr.univtln.bruno.samples.jaxrs.security;
package fr.univtln.bruno.samples.jaxrs.security.filter;
import fr.univtln.bruno.samples.jaxrs.security.MySecurityContext;
import fr.univtln.bruno.samples.jaxrs.security.annotations.BasicAuth;
import fr.univtln.bruno.samples.jaxrs.security.InMemoryLoginModule;
import jakarta.annotation.Priority;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
......@@ -10,24 +13,24 @@ 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;
/**
* The type Authentication filter is a JAX-RS filter (@Provider with implements ContainerRequestFilter) is applied to every request whose method is annotated with @BasicAuth
* as it is itself annotated with @BasicAuth (a personal annotation).
* It performs authentication and check permissions against the acceded method with a basic authentication.
*/
@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 {
public class BasicAuthenticationFilter implements ContainerRequestFilter {
private static final String AUTHORIZATION_PROPERTY = "Authorization";
private static final String AUTHENTICATION_SCHEME = "Basic";
......@@ -40,8 +43,8 @@ public class AuthenticationFilter implements ContainerRequestFilter {
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 it is PermitAll access is granted
//otherwise if it is DenyAll the access is refused
if (!method.isAnnotationPresent(PermitAll.class)) {
if (method.isAnnotationPresent(DenyAll.class)) {
requestContext.abortWith(Response.status(Response.Status.FORBIDDEN)
......@@ -59,64 +62,40 @@ public class AuthenticationFilter implements ContainerRequestFilter {
return;
}
//Get encoded username and password
//We extract the username and password encoded in base64
final String encodedUserPassword = authorization.substring(AUTHENTICATION_SCHEME.length()).trim();
//Decode username and password
String usernameAndPassword = new String(Base64.getDecoder().decode(encodedUserPassword.getBytes()));
//We Decode username and password (username:password)
String[] usernameAndPassword = new String(Base64.getDecoder().decode(encodedUserPassword.getBytes())).split(":");
//Split username and password tokens
final StringTokenizer tokenizer = new StringTokenizer(usernameAndPassword, ":");
final String username = tokenizer.nextToken();
final String password = tokenizer.nextToken();
final String username = usernameAndPassword[0];
final String password = usernameAndPassword[1];
log.info(username + " tries to log in with " + password);
log.info(username + " tries to log in");
//Verify user access
//We verify user access rights according to roles
//After Authentication we are doing Authorization
if (method.isAnnotationPresent(RolesAllowed.class)) {
RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
EnumSet<UserDatabase.Role> rolesSet =
EnumSet<InMemoryLoginModule.Role> rolesSet =
Arrays.stream(rolesAnnotation.value())
.map(r -> UserDatabase.Role.valueOf(r))
.collect(Collectors.toCollection(() -> EnumSet.noneOf(UserDatabase.Role.class)));
.map(InMemoryLoginModule.Role::valueOf)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(InMemoryLoginModule.Role.class)));
//We check to login/password
if (!UserDatabase.USER_DATABASE.checkPassword(username, password)) {
if (!InMemoryLoginModule.USER_DATABASE.login(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))) {
if (!InMemoryLoginModule.isInRoles(rolesSet, 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;
}
});
//We build a new SecurityContext Class to transmit the security data
// for this login attempt to JAX-RS
requestContext.setSecurityContext(MySecurityContext.newInstance(AUTHENTICATION_SCHEME, username));
}
}
......
package fr.univtln.bruno.samples.jaxrs.security;
package fr.univtln.bruno.samples.jaxrs.security.filter;
import fr.univtln.bruno.samples.jaxrs.security.InMemoryLoginModule;
import fr.univtln.bruno.samples.jaxrs.security.MySecurityContext;
import fr.univtln.bruno.samples.jaxrs.security.annotations.JWTAuth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import jakarta.annotation.Priority;
import jakarta.annotation.security.DenyAll;
......@@ -13,25 +17,21 @@ 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;
/**
* This class if a filter for JAX-RS to perform authentication via JWT.
*/
@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";
......@@ -40,32 +40,24 @@ public class JsonWebTokenFilter implements ContainerRequestFilter {
@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
//if its PermitAll access is granted (without specific security context)
if (method.isAnnotationPresent(PermitAll.class)) return;
//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
//We get the authorization header from the request
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)
......@@ -73,56 +65,46 @@ public class JsonWebTokenFilter implements ContainerRequestFilter {
return;
}
//Gets the token
log.info("AUTH: "+authorization);
//We get the token
final String compactJwt = authorization.substring(AUTHENTICATION_SCHEME.length()).trim();
if (!authorization.contains(AUTHENTICATION_SCHEME) || compactJwt == null || compactJwt.isEmpty()) {
if (!authorization.contains(AUTHENTICATION_SCHEME) || compactJwt.isEmpty()) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Please provide your credentials").build());
.entity("Please provide correct credentials").build());
return;
}
log.info("JWT: "+compactJwt);
String username = null;
//We check the validity of the token
try {
Jws<Claims> jws = Jwts.parserBuilder()
.setSigningKey(UserDatabase.KEY)
.requireIssuer("sample-jaxrs")
.setSigningKey(InMemoryLoginModule.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;
}
username = jws.getBody().getSubject();
//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);
requestContext.setSecurityContext(MySecurityContext.newInstance(AUTHENTICATION_SCHEME, username));
} catch (JwtException e) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Wrong JWT token. " + e.getLocalizedMessage()).build());
}
@Override
public boolean isUserInRole(String role) {
return UserDatabase.USER_DATABASE.getUserRoles(username).contains(UserDatabase.Role.valueOf(role));
}
@Override
public boolean isSecure() {
return true;
}
//If present we extract the allowed roles annotation.
if (method.isAnnotationPresent(RolesAllowed.class)) {
RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
EnumSet<InMemoryLoginModule.Role> rolesSet =
Arrays.stream(rolesAnnotation.value())
.map(InMemoryLoginModule.Role::valueOf)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(InMemoryLoginModule.Role.class)));
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
//We check if the role is allowed
if (!InMemoryLoginModule.isInRoles(rolesSet, username))
requestContext.abortWith(Response.status(Response.Status.FORBIDDEN)
.entity("Roles not allowed").build());
}
}
}
\ No newline at end of file
}
\ No newline at end of file
package fr.univtln.bruno.samples.jaxrs;
import fr.univtln.bruno.samples.jaxrs.model.BiblioModel.Auteur;
import fr.univtln.bruno.samples.jaxrs.security.InMemoryLoginModule;
import fr.univtln.bruno.samples.jaxrs.server.BiblioServer;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
......@@ -13,7 +19,11 @@ import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.message.internal.MediaTypes;
import org.junit.*;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import static org.junit.Assert.*;
......@@ -205,4 +215,72 @@ public class ServerIT {
assertEquals(1, auteurs.size());
assertEquals("Marie", auteurs.get(0).getPrenom());
}
@Test
public void refusedLogin() {
Response result = webTarget.path("biblio/login")
.request()
.get();
assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), result.getStatus());
}
@Test
public void acceptedLogin() {
String email="john.doe@nowhere.com";
String password="admin";
Response result = webTarget.path("biblio/login")
.request()
.accept(MediaType.TEXT_PLAIN)
.header("Authorization", "Basic "+java.util.Base64.getEncoder().encodeToString((email+":"+password).getBytes()))
.get();
String entity = result.readEntity(String.class);
assertEquals(Response.Status.OK.getStatusCode(), result.getStatus());
Jws<Claims> jws = Jwts.parserBuilder()
.setSigningKey(InMemoryLoginModule.KEY)
.build()
.parseClaimsJws(entity);
assertEquals(email,jws.getBody().getSubject());
}
@Test
public void jwtAccess() {
//Log in to get the token
String email="john.doe@nowhere.com";
String password="admin";
String token = webTarget.path("biblio/login")
.request()
.accept(MediaType.TEXT_PLAIN)
.header("Authorization", "Basic "+java.util.Base64.getEncoder().encodeToString((email+":"+password).getBytes()))
.get(String.class);
//We access a JWT protected URL with the token
Response result = webTarget.path("biblio/secured")
.request()
.header( "Authorization", "Bearer "+token)
.get();
assertEquals(Response.Status.OK.getStatusCode(), result.getStatus());
assertEquals("Access with JWT ok for Doe, John <john.doe@nowhere.com>",result.readEntity(String.class));
}
@Test
public void jwtAccessDenied() {
String forgedToken = Jwts.builder()
.setIssuer("sample-jaxrs")
.setIssuedAt(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
.setSubject("john.doe@nowhere.com")
.claim("firstname", "John")
.claim("lastname", "Doe")
.setExpiration(Date.from(LocalDateTime.now().plus(15, ChronoUnit.MINUTES).atZone(ZoneId.systemDefault()).toInstant()))
//A RANDOM KEY DIFFERENT FROM THE SERVER
.signWith( Keys.secretKeyFor(SignatureAlgorithm.HS256)).compact();
//We access a JWT protected URL with the token
Response result = webTarget.path("biblio/secured")
.request()
.header( "Authorization", "Bearer "+forgedToken)
.get();
assertNotEquals(Response.Status.OK.getStatusCode(), result.getStatus());
}
}
package fr.univtln.bruno.samples.jaxrs.security;
import org.junit.Test;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.EnumSet;
import static org.junit.Assert.assertTrue;
public class UserTest {
@Test
public void testBuilder() throws InvalidKeySpecException, NoSuchAlgorithmException {
String lastname="Doe", firstname="John", email="j.d@here.com", password="mypass";
User user = User.builder()
.lastName(lastname)
.firstName(firstname)
.email(email)
.password(password)
.roles(EnumSet.of(InMemoryLoginModule.Role.ADMIN))
.build();
assertTrue(user.checkPassword(password));
assertTrue(user.contains(InMemoryLoginModule.Role.valueOf("ADMIN")));
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment