ditch access keys
This commit is contained in:
parent
63072a8af0
commit
b108f7688a
14 changed files with 238 additions and 304 deletions
|
@ -13,34 +13,24 @@ import eu.m724.orm.AccessLimits;
|
||||||
* if you were to sell access you can "tweak" some of this and rip off people
|
* if you were to sell access you can "tweak" some of this and rip off people
|
||||||
*/
|
*/
|
||||||
public class GlobalAccessLimits {
|
public class GlobalAccessLimits {
|
||||||
|
public static AccessLimits kilo, mega, giga, tera;
|
||||||
|
|
||||||
public static void initialize() {
|
public static void initialize() {
|
||||||
AccessLimits kilo = new AccessLimits();
|
AccessLimits kilo = new AccessLimits("kilo", true, true, 60);
|
||||||
kilo.label = "kilo";
|
|
||||||
kilo.thunder = true;
|
|
||||||
kilo.weather = true;
|
|
||||||
kilo.weatherRequestsHourly = 60;
|
|
||||||
|
|
||||||
AccessLimits mega = new AccessLimits();
|
|
||||||
mega.label = "plus";
|
|
||||||
mega.thunder = true;
|
|
||||||
mega.weather = true;
|
|
||||||
mega.weatherRequestsHourly = 120;
|
|
||||||
|
|
||||||
AccessLimits giga = new AccessLimits();
|
|
||||||
giga.label = "giga";
|
|
||||||
giga.thunder = true;
|
|
||||||
giga.weather = true;
|
|
||||||
giga.weatherRequestsHourly = 240;
|
|
||||||
|
|
||||||
AccessLimits tera = new AccessLimits();
|
|
||||||
tera.label = "giga";
|
|
||||||
tera.thunder = true;
|
|
||||||
tera.weather = true;
|
|
||||||
tera.weatherRequestsHourly = 480;
|
|
||||||
|
|
||||||
kilo.persist();
|
kilo.persist();
|
||||||
|
|
||||||
|
AccessLimits mega = new AccessLimits("mega", true, true, 120);
|
||||||
mega.persist();
|
mega.persist();
|
||||||
|
|
||||||
|
AccessLimits giga = new AccessLimits("giga", true, true, 240);
|
||||||
giga.persist();
|
giga.persist();
|
||||||
|
|
||||||
|
AccessLimits tera = new AccessLimits("tera", true, true, 480);
|
||||||
tera.persist();
|
tera.persist();
|
||||||
|
|
||||||
|
GlobalAccessLimits.kilo = kilo;
|
||||||
|
GlobalAccessLimits.mega = mega;
|
||||||
|
GlobalAccessLimits.giga = giga;
|
||||||
|
GlobalAccessLimits.tera = tera;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
39
src/main/java/eu/m724/GlobalExceptionHandler.java
Normal file
39
src/main/java/eu/m724/GlobalExceptionHandler.java
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package eu.m724;
|
||||||
|
|
||||||
|
import jakarta.json.Json;
|
||||||
|
import jakarta.json.JsonObject;
|
||||||
|
import jakarta.json.stream.JsonParsingException;
|
||||||
|
import jakarta.ws.rs.ClientErrorException;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||||
|
import jakarta.ws.rs.ext.Provider;
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(Throwable exception) {
|
||||||
|
System.out.println("andling error" + exception.getMessage());
|
||||||
|
|
||||||
|
int status = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
|
||||||
|
String message = "An error has occurred";
|
||||||
|
|
||||||
|
if (exception instanceof ClientErrorException) {
|
||||||
|
status = ((ClientErrorException) exception).getResponse().getStatus();
|
||||||
|
message = exception.getMessage();
|
||||||
|
} else if (exception instanceof JsonParsingException) {
|
||||||
|
status = Response.Status.BAD_REQUEST.getStatusCode();
|
||||||
|
message = "Valid JSON expected"; // TODO make this error better
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject errorJson = Json.createObjectBuilder()
|
||||||
|
.add("status", status)
|
||||||
|
.add("message", message)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Response.status(status)
|
||||||
|
.entity(errorJson)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
package eu.m724;
|
|
||||||
|
|
||||||
import eu.m724.auth.master.AccountService;
|
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* managing access keys (not master keys or accounts)
|
|
||||||
*/
|
|
||||||
@Path("/api/keys")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public class KeysResource {
|
|
||||||
@Inject
|
|
||||||
SecurityIdentity securityIdentity;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountService accountService;
|
|
||||||
}
|
|
|
@ -1,10 +1,8 @@
|
||||||
package eu.m724;
|
package eu.m724;
|
||||||
|
|
||||||
import eu.m724.auth.master.AccountService;
|
import eu.m724.orm.Token;
|
||||||
import eu.m724.orm.Account;
|
|
||||||
import io.quarkus.runtime.StartupEvent;
|
import io.quarkus.runtime.StartupEvent;
|
||||||
import jakarta.enterprise.event.Observes;
|
import jakarta.enterprise.event.Observes;
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
@ -12,15 +10,18 @@ import java.util.Base64;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class Startup {
|
public class Startup {
|
||||||
@Inject
|
|
||||||
AccountService accountService;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void loadUsers(@Observes StartupEvent ignoredEvent) {
|
public void loadUsers(@Observes StartupEvent ignoredEvent) {
|
||||||
Account.deleteAll();
|
GlobalAccessLimits.initialize();
|
||||||
|
Token.deleteAll();
|
||||||
byte[] adminKey = new byte[18];
|
byte[] adminKey = new byte[18];
|
||||||
|
|
||||||
accountService.add(adminKey, "admin");
|
Token token = new Token();
|
||||||
System.out.println("Admin user created: " + Base64.getEncoder().encodeToString(adminKey));
|
token.accessLimits = GlobalAccessLimits.kilo;
|
||||||
|
token.role = "admin";
|
||||||
|
token.tokenBytes = adminKey;
|
||||||
|
token.persist();
|
||||||
|
|
||||||
|
System.out.println("Admin token created: " + Base64.getEncoder().encodeToString(adminKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
57
src/main/java/eu/m724/TokensResource.java
Normal file
57
src/main/java/eu/m724/TokensResource.java
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package eu.m724;
|
||||||
|
|
||||||
|
import eu.m724.orm.AccessLimits;
|
||||||
|
import eu.m724.orm.Token;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.json.Json;
|
||||||
|
import jakarta.json.JsonObject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
@Path("/api/tokens")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public class TokensResource {
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/create")
|
||||||
|
@RolesAllowed("admin")
|
||||||
|
public JsonObject createToken(JsonObject data) {
|
||||||
|
String label = data.getString("accessLimits", null);
|
||||||
|
if (label == null) {
|
||||||
|
throw new BadRequestException("Specify access limits in the 'accessLimits' key");
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessLimits accessLimits = AccessLimits.findByLabel(label);
|
||||||
|
if (accessLimits == null) {
|
||||||
|
throw new BadRequestException("Unknown access limits: " + label);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String token = Token.generate(accessLimits);
|
||||||
|
|
||||||
|
return Json.createObjectBuilder()
|
||||||
|
.add("token", token)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/me")
|
||||||
|
@RolesAllowed({"user", "admin"})
|
||||||
|
public JsonObject me() {
|
||||||
|
Token token = securityIdentity.getAttribute("token");
|
||||||
|
String tokenEncoded = securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
|
String censoredToken = tokenEncoded.substring(0, 5) + "..." + tokenEncoded.substring(tokenEncoded.length() - 5);
|
||||||
|
|
||||||
|
return Json.createObjectBuilder()
|
||||||
|
.add("token", censoredToken)
|
||||||
|
.add("role", token.role)
|
||||||
|
.add("accessLimits", token.accessLimits.label)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,50 +0,0 @@
|
||||||
package eu.m724;
|
|
||||||
|
|
||||||
import eu.m724.auth.master.AccountService;
|
|
||||||
import eu.m724.orm.Account;
|
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.json.Json;
|
|
||||||
import jakarta.json.JsonObject;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
|
|
||||||
@Path("/api/users")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public class UsersResource {
|
|
||||||
@Inject
|
|
||||||
SecurityIdentity securityIdentity;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountService accountService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/create")
|
|
||||||
@RolesAllowed("admin")
|
|
||||||
public JsonObject createAccount() {
|
|
||||||
String masterKey = accountService.create("user");
|
|
||||||
|
|
||||||
return Json.createObjectBuilder()
|
|
||||||
.add("masterKey", masterKey)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/me")
|
|
||||||
@RolesAllowed({"user", "admin"})
|
|
||||||
public JsonObject me() {
|
|
||||||
Account account = securityIdentity.getAttribute("account");
|
|
||||||
String masterKey = securityIdentity.getPrincipal().getName();
|
|
||||||
|
|
||||||
String censoredKey = masterKey.substring(0, 5) + "..." + masterKey.substring(masterKey.length() - 5);
|
|
||||||
|
|
||||||
return Json.createObjectBuilder()
|
|
||||||
.add("masterKey", censoredKey)
|
|
||||||
.add("role", account.role)
|
|
||||||
.add("accessKeys", account.accessKeys.size())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package eu.m724.auth.master;
|
|
||||||
|
|
||||||
import eu.m724.orm.AccessKey;
|
|
||||||
import eu.m724.orm.AccessLimits;
|
|
||||||
import eu.m724.orm.Account;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class AccessKeyService {
|
|
||||||
private final SecureRandom random = new SecureRandom();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* generates an access key for an account
|
|
||||||
* @param account the account
|
|
||||||
* @param accessLimits access limits
|
|
||||||
* @return base64 encoded access key
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public String createAccessKey(Account account, AccessLimits accessLimits) {
|
|
||||||
byte[] key = new byte[18];
|
|
||||||
random.nextBytes(key);
|
|
||||||
|
|
||||||
AccessKey accessKey = new AccessKey();
|
|
||||||
accessKey.key = key;
|
|
||||||
accessKey.account = account;
|
|
||||||
accessKey.accessLimits = accessLimits;
|
|
||||||
|
|
||||||
account.accessKeys.add(accessKey);
|
|
||||||
account.persist();
|
|
||||||
|
|
||||||
return Base64.getEncoder().encodeToString(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void deleteAccessKey(AccessKey accessKey) {
|
|
||||||
accessKey.account = null; // TODO hopefully that works
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
package eu.m724.auth.master;
|
|
||||||
|
|
||||||
import eu.m724.orm.AccessKey;
|
|
||||||
import eu.m724.orm.Account;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class AccountService {
|
|
||||||
private final SecureRandom random = new SecureRandom();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* find a master user by key
|
|
||||||
* @param key base64 encoded key
|
|
||||||
* @return the master user or null if wrong key or key is null
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public Account findByKey(String key) {
|
|
||||||
if (key == null) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return Account.find("masterKey", (Object) Base64.getDecoder().decode(key)).firstResult();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gets an access key
|
|
||||||
* @param bytes access key as bytes
|
|
||||||
* @return the {@link AccessKey} if correct else null even if key is null
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public AccessKey findByAccessKey(byte[] bytes) {
|
|
||||||
if (bytes == null) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return AccessKey.find("key", (Object) bytes).firstResult();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO maybe move some of these methods somewhere else and reconsider making them static
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates an account with the specified key
|
|
||||||
* @param masterKey the desired master key
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public void add(byte[] masterKey, String role) {
|
|
||||||
Account account = new Account();
|
|
||||||
account.masterKey = masterKey;
|
|
||||||
account.role = role;
|
|
||||||
account.persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates an account with random key
|
|
||||||
* @param role new account's role
|
|
||||||
* @return base64 encoded key
|
|
||||||
*/
|
|
||||||
public String create(String role) {
|
|
||||||
byte[] key = new byte[18]; // 144 bits of entropy
|
|
||||||
random.nextBytes(key);
|
|
||||||
|
|
||||||
add(key, role);
|
|
||||||
return Base64.getEncoder().encodeToString(key);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
package eu.m724.auth.master;
|
package eu.m724.auth.master;
|
||||||
|
|
||||||
import eu.m724.orm.Account;
|
import eu.m724.orm.Token;
|
||||||
import io.quarkus.security.identity.IdentityProviderManager;
|
import io.quarkus.security.identity.IdentityProviderManager;
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
import io.quarkus.security.runtime.QuarkusPrincipal;
|
import io.quarkus.security.runtime.QuarkusPrincipal;
|
||||||
|
@ -11,27 +11,23 @@ import io.smallrye.mutiny.Uni;
|
||||||
import io.smallrye.mutiny.infrastructure.Infrastructure;
|
import io.smallrye.mutiny.infrastructure.Infrastructure;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class MyHttpAuthenticationMechanism implements HttpAuthenticationMechanism {
|
public class MyHttpAuthenticationMechanism implements HttpAuthenticationMechanism {
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountService accountService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
|
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
|
||||||
String encodedKey = context.request().getHeader("X-Master-Key");
|
String encodedToken = context.request().getHeader("X-Token");
|
||||||
|
|
||||||
return Uni.createFrom().item(() -> {
|
return Uni.createFrom().item(() -> {
|
||||||
Account account = accountService.findByKey(encodedKey);
|
Token token = Token.findByToken(encodedToken);
|
||||||
|
|
||||||
if (account != null) {
|
if (token != null) {
|
||||||
QuarkusSecurityIdentity identity = QuarkusSecurityIdentity.builder()
|
QuarkusSecurityIdentity identity = QuarkusSecurityIdentity.builder()
|
||||||
.setPrincipal(new QuarkusPrincipal(encodedKey))
|
.setPrincipal(new QuarkusPrincipal(encodedToken))
|
||||||
.addRole(account.role)
|
.addRole(token.role)
|
||||||
.addAttribute("account", account)
|
.addAttribute("token", token)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return (SecurityIdentity) identity;
|
return (SecurityIdentity) identity;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -41,6 +37,6 @@ public class MyHttpAuthenticationMechanism implements HttpAuthenticationMechanis
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uni<ChallengeData> getChallenge(RoutingContext context) {
|
public Uni<ChallengeData> getChallenge(RoutingContext context) {
|
||||||
return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "X-Master-Key"));
|
return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "X-Token"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
package eu.m724.orm;
|
|
||||||
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.ManyToOne;
|
|
||||||
|
|
||||||
//@Entity
|
|
||||||
public class AccessKey extends PanacheEntity {
|
|
||||||
/**
|
|
||||||
* raw bytes of this key, it's provided to users in base64
|
|
||||||
*/
|
|
||||||
@Column(unique = true)
|
|
||||||
public byte[] key;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* access limits of this key
|
|
||||||
*/
|
|
||||||
public AccessLimits accessLimits;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the user owning this access key
|
|
||||||
*/
|
|
||||||
@ManyToOne
|
|
||||||
public Account account;
|
|
||||||
}
|
|
|
@ -6,6 +6,15 @@ import jakarta.persistence.Entity;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class AccessLimits extends PanacheEntity {
|
public class AccessLimits extends PanacheEntity {
|
||||||
|
public AccessLimits() {}
|
||||||
|
|
||||||
|
public AccessLimits(String label, boolean thunder, boolean weather, int weatherRequestsHourly) {
|
||||||
|
this.label = label;
|
||||||
|
this.thunder = thunder;
|
||||||
|
this.weather = weather;
|
||||||
|
this.weatherRequestsHourly = weatherRequestsHourly;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* label of these limits, displayed to user and used to identify the limits
|
* label of these limits, displayed to user and used to identify the limits
|
||||||
*/
|
*/
|
||||||
|
@ -28,4 +37,13 @@ public class AccessLimits extends PanacheEntity {
|
||||||
* max requests per hour
|
* max requests per hour
|
||||||
*/
|
*/
|
||||||
public int weatherRequestsHourly;
|
public int weatherRequestsHourly;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find {@link AccessLimits} by label
|
||||||
|
* @param label label
|
||||||
|
* @return {@link AccessLimits} or null if not found
|
||||||
|
*/
|
||||||
|
public static AccessLimits findByLabel(String label) {
|
||||||
|
return AccessLimits.find("label", label).firstResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
package eu.m724.orm;
|
|
||||||
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
|
||||||
import io.quarkus.security.jpa.Roles;
|
|
||||||
import jakarta.persistence.CascadeType;
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.OneToMany;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
// TODO organize all this like work on variable names move functions etc
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
public class Account extends PanacheEntity {
|
|
||||||
@Column(unique = true)
|
|
||||||
public byte[] masterKey;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
public Set<AccessKey> accessKeys = new HashSet<>();
|
|
||||||
|
|
||||||
@Roles
|
|
||||||
public String role = "user";
|
|
||||||
}
|
|
68
src/main/java/eu/m724/orm/Token.java
Normal file
68
src/main/java/eu/m724/orm/Token.java
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package eu.m724.orm;
|
||||||
|
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||||
|
import io.quarkus.security.jpa.Roles;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
// TODO organize all this like work on variable names move functions etc
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class Token extends PanacheEntity {
|
||||||
|
@Column(unique = true)
|
||||||
|
public byte[] tokenBytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* access limits of this key
|
||||||
|
*/
|
||||||
|
@ManyToOne
|
||||||
|
public AccessLimits accessLimits;
|
||||||
|
|
||||||
|
@Roles
|
||||||
|
public String role = "user";
|
||||||
|
|
||||||
|
// TODO maybe move some stuff
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a random token
|
||||||
|
* @return base64 encoded key
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public static String generate(AccessLimits accessLimits) {
|
||||||
|
byte[] tokenBytes = new byte[18]; // 144 bits of entropy
|
||||||
|
new SecureRandom().nextBytes(tokenBytes);
|
||||||
|
|
||||||
|
Token token = new Token();
|
||||||
|
token.tokenBytes = tokenBytes;
|
||||||
|
token.accessLimits = accessLimits;
|
||||||
|
token.persist();
|
||||||
|
|
||||||
|
return Base64.getEncoder().encodeToString(tokenBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find an {@link Token} by master key
|
||||||
|
* @param tokenBytes token in bytes
|
||||||
|
* @return an {@link Token} or null if not found
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public static Token findByToken(byte[] tokenBytes) {
|
||||||
|
return Token.find("tokenBytes", (Object) tokenBytes).firstResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find an {@link Token} by master key
|
||||||
|
* @param tokenEncoded base64 encoded token
|
||||||
|
* @return an {@link Token} or null if not found
|
||||||
|
* @throws IllegalArgumentException if base64 is invalid
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public static Token findByToken(String tokenEncoded) {
|
||||||
|
return findByToken(Base64.getDecoder().decode(tokenEncoded));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
package eu.m724.websocket;
|
package eu.m724.websocket;
|
||||||
|
|
||||||
import eu.m724.auth.master.AccountService;
|
import eu.m724.orm.Token;
|
||||||
import eu.m724.orm.AccessKey;
|
|
||||||
import eu.m724.websocket.packet.DisconnectReason;
|
import eu.m724.websocket.packet.DisconnectReason;
|
||||||
import eu.m724.websocket.packet.clientbound.PongPacket;
|
import eu.m724.websocket.packet.clientbound.PongPacket;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.websocket.Session;
|
import jakarta.websocket.Session;
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
|
@ -18,39 +16,42 @@ public class WebsocketService {
|
||||||
@ConfigProperty(name = "rwws.protocol_version")
|
@ConfigProperty(name = "rwws.protocol_version")
|
||||||
byte protocolVersion;
|
byte protocolVersion;
|
||||||
|
|
||||||
@Inject
|
private final Map<String, Token> tokens = new ConcurrentHashMap<>();
|
||||||
AccountService accountService;
|
|
||||||
|
|
||||||
private final Map<String, AccessKey> accounts = new ConcurrentHashMap<>();
|
//
|
||||||
|
|
||||||
void addSession(String sessionId) {
|
void addSession(String sessionId) {
|
||||||
accounts.put(sessionId, null);
|
tokens.put(sessionId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeConnection(String sessionId) {
|
void removeConnection(String sessionId) {
|
||||||
accounts.remove(sessionId);
|
tokens.remove(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
boolean authenticate(String sessionId, byte[] tokenBytes) {
|
||||||
|
Token token = Token.findByToken(tokenBytes);
|
||||||
|
|
||||||
|
if (token == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
tokens.put(sessionId, token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isAuthenticated(String sessionId) {
|
||||||
|
return tokens.containsKey(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
void disconnect(Session session, DisconnectReason reason, String message) {
|
void disconnect(Session session, DisconnectReason reason, String message) {
|
||||||
try {
|
try {
|
||||||
session.close(reason.asCloseReason(message));
|
session.close(reason.asCloseReason(message));
|
||||||
} catch (IOException ignored) { }
|
} catch (IOException ignored) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean authenticate(String sessionId, byte[] bytes) {
|
|
||||||
AccessKey accessKey = accountService.findByAccessKey(bytes);
|
|
||||||
|
|
||||||
if (accessKey == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
accounts.put(sessionId, accessKey);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isAuthenticated(String sessionId) {
|
|
||||||
return accounts.containsKey(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void pong(Session session) {
|
void pong(Session session) {
|
||||||
new PongPacket().send(session);
|
new PongPacket().send(session);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue