[DEV] add a secure way to access on the public kay with application token

This commit is contained in:
Edouard DUPIN 2023-01-28 00:17:07 +01:00
parent ede555cb30
commit c0ab443c43
9 changed files with 282 additions and 53 deletions

View File

@ -27,7 +27,7 @@
<dependency>
<groupId>kangaroo-and-rabbit</groupId>
<artifactId>archidata</artifactId>
<version>0.2.4</version>
<version>0.3.1</version>
</dependency>
<!-- testing -->
<dependency>

View File

@ -9,7 +9,9 @@ import org.kar.karso.api.HealthCheck;
import org.kar.karso.api.PublicKeyResource;
import org.kar.karso.api.SystemConfigResource;
import org.kar.karso.api.UserResource;
import org.kar.karso.filter.KarsoAuthenticationFilter;
import org.kar.karso.model.Application;
import org.kar.karso.model.ApplicationToken;
import org.kar.karso.model.Settings;
import org.kar.karso.model.UserAuth;
//import org.kar.archidata.model.Migration;
@ -55,6 +57,7 @@ public class WebLauncher {
out += SqlWrapper.createTable(Settings.class);
out += SqlWrapper.createTable(UserAuth.class);
out += SqlWrapper.createTable(Application.class);
out += SqlWrapper.createTable(ApplicationToken.class);
//out += SqlWrapper.createTable(Migration.class);
System.out.println(out);
} catch (Exception e) {
@ -69,7 +72,7 @@ public class WebLauncher {
rc.register(new OptionFilter());
// remove cors ==> all time called by an other system...
rc.register(new CORSFilter());
rc.registerClasses(AuthenticationFilter.class);
rc.registerClasses(KarsoAuthenticationFilter.class);
// register exception catcher
rc.register(InputExceptionCatcher.class);
rc.register(SystemExceptionCatcher.class);

View File

@ -26,28 +26,16 @@ public class ApplicationResource {
@GET
@RolesAllowed(value= {"USER", "ADMIN"})
public List<Application> getApplications() {
public List<Application> getApplications() throws Exception {
System.out.println("getApplications");
try {
return SqlWrapper.gets(Application.class, false);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
return SqlWrapper.gets(Application.class, false);
}
@GET
@Path("small")
@RolesAllowed(value= {"USER", "ADMIN"})
public List<ApplicationSmall> getApplicationsSmall() {
public List<ApplicationSmall> getApplicationsSmall() throws Exception {
System.out.println("getApplications");
try {
return SqlWrapper.gets(ApplicationSmall.class, false);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
return SqlWrapper.gets(ApplicationSmall.class, false);
}

View File

@ -0,0 +1,171 @@
package org.kar.karso.api;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.SqlWrapper;
import org.kar.archidata.WhereCondition;
import org.kar.archidata.filter.GenericContext;
import org.kar.archidata.model.Data;
import org.kar.karso.model.*;
import org.kar.archidata.util.JWTWrapper;
import org.kar.archidata.annotation.security.PermitAll;
import org.kar.archidata.annotation.security.RolesAllowed;
import org.kar.archidata.api.DataResource;
import org.kar.archidata.exception.InputException;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
@Path("/application_token")
@Produces( MediaType.APPLICATION_JSON)
public class ApplicationTokenResource {
public ApplicationTokenResource() {
}
@GET
@Path("{applicationId}")
@RolesAllowed(value= {"ADMIN"})
public List<ApplicationToken> gets(@Context SecurityContext sc, @PathParam("applicationId") Long applicationId) throws Exception {
List<ApplicationToken> values = SqlWrapper.getsWhere(ApplicationToken.class,
List.of(
new WhereCondition("parentId", "=", applicationId)
),
false);
// clean all tokens this is a secret:
for (ApplicationToken elem : values) {
elem.token = null;
}
return values;
}
@DELETE
@Path("{applicationId}/{tokenId}")
@RolesAllowed(value= {"ADMIN"})
public Response delete(
@Context SecurityContext sc,
@PathParam("applicationId") Long applicationId,
@PathParam("tokenId") Integer tokenId) throws Exception {
int nbRemoved = SqlWrapper.setDeleteWhere(ApplicationToken.class,
List.of(
new WhereCondition("parentId", "=", applicationId),
new WhereCondition("tokenId", "=", tokenId)
)
);
if (nbRemoved == 0) {
return Response.notModified("{}").build();
}
if (nbRemoved == 0) {
return Response.serverError().build();
}
return Response.ok("{}").build();
}
private String multipartCorrection(String data) {
if (data == null) {
return null;
}
if (data.isEmpty()) {
return null;
}
if (data.contentEquals("null")) {
return null;
}
return data;
}
static String randomToken() {
int len = 48;
String valid_element = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvxyz0123456789#_@-|[])('~$%*/\\.!?,";
// creating a StringBuffer size of AlphaNumericStr
StringBuilder out = new StringBuilder(len);
int iii;
for (iii=0; iii<len; iii++) {
//generating a random number using math.random()
int ch = (int)(valid_element.length() * Math.random());
//adding Random character one by one at the end of s
out.append(valid_element.charAt(ch));
}
return out.toString();
}
@POST
@Path("/{applicationId}/create")
@RolesAllowed("ADMIN")
public ApplicationToken createToken(
@Context SecurityContext sc,
@PathParam("applicationId") Long applicationId,
@FormDataParam("name") String name,
@FormDataParam("validity") Integer validity
) throws Exception {
// correct input string stream :
name = multipartCorrection(name);
//validity = multipartCorrection(validity);
if (applicationId == null) {
throw new InputException("applicationId", "can not be null");
}
int maximum = 365*5;
if (validity == null || validity < 0 || validity > maximum) {
validity = maximum;
}
// todo: set validity timestamp ...
ApplicationToken token = new ApplicationToken();
token.token = randomToken();
token.name = multipartCorrection(name);
token.parentId = applicationId;
OffsetDateTime now = OffsetDateTime.now( ZoneOffset.UTC );
token.endValidityTime = Timestamp.from(now.plusDays(validity).toInstant());
// insert in the BDD
token = SqlWrapper.insert(token);
// here we return the token to permit to the user to see it to set it in the application.
return token;
}
}

View File

@ -5,7 +5,7 @@ import javax.ws.rs.*;
import org.kar.archidata.api.FrontGeneric;
import org.kar.karso.util.ConfigVariable;
@Path("/karso")
@Path("/front")
public class Front extends FrontGeneric {
public Front() {
this.baseFrontFolder = ConfigVariable.getFrontFolder();

View File

@ -3,53 +3,43 @@ package org.kar.karso.api;
import org.kar.archidata.util.JWTWrapper;
import org.kar.archidata.util.JWTWrapper.PublicKey;
import com.nimbusds.jose.JOSEException;
import org.kar.archidata.annotation.security.PermitAll;
import org.kar.archidata.annotation.security.RolesAllowed;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@Path("/public_key")
@Produces(MediaType.APPLICATION_JSON)
public class PublicKeyResource {
public PublicKeyResource() {
}
// This is for java server that use the same implementation
// curl http://localhost:9993/public_key
@GET
@PermitAll
@RolesAllowed(value= {"APPLICATION"})
public PublicKey getKey() {
return new PublicKey(JWTWrapper.getPublicKey());
return new PublicKey(JWTWrapper.getPublicKeyJson());
}
// This is for common other interface that support public PEM stream.
// curl http://localhost:9993/public_key/pem
@GET
@Path("/pem")
@RolesAllowed(value= {"APPLICATION"})
public String getKeyPem() throws JOSEException {
RSAPublicKey keyPub = JWTWrapper.getPublicKeyJava();
byte[] data = keyPub.getEncoded();
String base64encoded = new String(Base64.getEncoder().encode(data));
return "-----BEGIN PUBLIC KEY-----\n" + base64encoded + "\n-----END PUBLIC KEY-----\n";
}
}

View File

@ -68,7 +68,7 @@ public class UserResource {
user.blocked = data;
int ret = SqlWrapper.update(user, userId, List.of("blocked"));
if (ret == 0) {
return Response.notModified("{}").build();
return Response.notModified("{}").build();
}
return Response.ok("{}").build();
}

View File

@ -0,0 +1,63 @@
package org.kar.karso.filter;
import java.sql.Timestamp;
import java.time.Instant;
import org.kar.archidata.filter.AuthenticationFilter;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.ext.Provider;
import org.kar.archidata.SqlWrapper;
import org.kar.archidata.model.UserByToken;
import org.kar.karso.model.ApplicationToken;
//@PreMatching
@Provider
@Priority(Priorities.AUTHENTICATION)
public class KarsoAuthenticationFilter extends AuthenticationFilter {
//curl http://0.0.0.0:15080/karso/api/public_key/pem --output plop.txt -H "Authorization: Zota 1:U0sJM1m@-STSdfg4365fJOFUGbR4kFycBu1qGZPwf7gW6k2WWRBzTPUH7QutCgPw-SDss45_563sSDFdfg@dsf@456" --verbose
@Override
protected UserByToken validateToken(String authorization) throws Exception {
if (authorization == null || authorization.length() < 25) {
System.out.println("Application authentication too short '" + authorization + "'");
return null;
}
String[] elems = authorization.split(":");
if (elems.length != 2) {
System.out.println("Application authentication split error '" + authorization + "'");
return null;
}
Long indexToken = Long.parseLong(elems[0]);
ApplicationToken value = SqlWrapper.get(ApplicationToken.class, indexToken);
if (value == null) {
System.out.println("Application authentication can not find id '" + authorization + "'");
return null;
}
// TODO: hash the sent token
if (!value.token.equals(elems[1])) {
System.out.println("Application authentication mismatch '" + authorization + "'");
return null;
}
// TODO: check UTC !!!
if (!value.endValidityTime.after(Timestamp.from(Instant.now()))) {
System.out.println("Application authentication Time-out '" + authorization + "' " + value.endValidityTime + " > " + Timestamp.from(Instant.now()));
return null;
}
// ----------------------------------
// -- All is good !!!
// ----------------------------------
// We are in transition phase the user element will be removed
UserByToken userByToken = new UserByToken();
userByToken.id = value.id;
userByToken.name = value.name;
userByToken.parentId = value.parentId;
userByToken.type = UserByToken.TYPE_APPLICATION;
userByToken.right.put("APPLICATION", true);
return userByToken;
}
}

View File

@ -0,0 +1,14 @@
package org.kar.karso.model;
import org.kar.archidata.annotation.SQLIfNotExists;
import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.model.GenericToken;
import com.fasterxml.jackson.annotation.JsonInclude;
@SQLTableName ("applicationToken")
@SQLIfNotExists
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApplicationToken extends GenericToken {
}