♻️ Transform to IDP provider
This commit is contained in:
parent
7cdf27f372
commit
79104dd02f
13
pom.xml
13
pom.xml
@ -57,7 +57,6 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-elytron-security</artifactId>
|
<artifactId>quarkus-elytron-security</artifactId>
|
||||||
<version>3.26.2</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
@ -79,6 +78,18 @@
|
|||||||
<groupId>org.flywaydb</groupId>
|
<groupId>org.flywaydb</groupId>
|
||||||
<artifactId>flyway-database-postgresql</artifactId>
|
<artifactId>flyway-database-postgresql</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
<version>3.20.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||||
|
<version>2.21.1</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-junit5</artifactId>
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
|
|||||||
11
postgres.yaml
Normal file
11
postgres.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: iam-dev-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
@ -1,93 +1,36 @@
|
|||||||
package de.tavolio;
|
package de.tavolio;
|
||||||
|
|
||||||
import de.tavolio.account.AccountEntity;
|
import de.tavolio.realm.user.UserEntity;
|
||||||
import de.tavolio.account.AccountRepo;
|
import de.tavolio.realm.user.UserRepo;
|
||||||
import io.quarkus.security.UnauthorizedException;
|
import io.quarkus.security.UnauthorizedException;
|
||||||
import io.vertx.core.http.HttpServerRequest;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.NotFoundException;
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import jakarta.ws.rs.core.HttpHeaders;
|
|
||||||
import jakarta.ws.rs.core.SecurityContext;
|
import jakarta.ws.rs.core.SecurityContext;
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class AuthenticationService
|
public class AuthenticationService
|
||||||
{
|
{
|
||||||
@Inject
|
@Inject
|
||||||
Logger LOG;
|
UserRepo userRepo;
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountRepo accountRepo;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityContext securityContext;
|
SecurityContext securityContext;
|
||||||
|
|
||||||
@Inject
|
public UserEntity requireUser()
|
||||||
HttpServerRequest request;
|
|
||||||
|
|
||||||
@ConfigProperty(name = "iam.user.name")
|
|
||||||
String superuserName;
|
|
||||||
|
|
||||||
@ConfigProperty(name = "iam.user.password")
|
|
||||||
String superuserPassword;
|
|
||||||
|
|
||||||
public boolean isSuperUser()
|
|
||||||
{
|
|
||||||
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
|
||||||
if (authHeader != null && !authHeader.isBlank())
|
|
||||||
{
|
|
||||||
String[] sections = authHeader.split("\\s+");
|
|
||||||
if (sections.length == 2 && sections[0].equals("Basic"))
|
|
||||||
{
|
|
||||||
String value = new String(Base64.getDecoder().decode(sections[1]));
|
|
||||||
String[] parts = value.split(":");
|
|
||||||
if (parts.length == 2)
|
|
||||||
{
|
|
||||||
String username = parts[0];
|
|
||||||
String password = parts[1];
|
|
||||||
boolean isSuperuser = username.equals(superuserName) && password.equals(superuserPassword);
|
|
||||||
if (isSuperuser)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
LOG.errorf("Invalid username or password", getRequestPath());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG.errorf("Invalid base64 credentials %s", getRequestPath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity requireUser()
|
|
||||||
{
|
{
|
||||||
Principal principal = securityContext.getUserPrincipal();
|
Principal principal = securityContext.getUserPrincipal();
|
||||||
if(principal != null)
|
if(principal != null)
|
||||||
{
|
{
|
||||||
AccountEntity accountEntity = accountRepo.findById(principal.getName());
|
UserEntity userEntity = userRepo.findById(principal.getName());
|
||||||
if(accountEntity != null)
|
if(userEntity != null)
|
||||||
{
|
{
|
||||||
return accountEntity;
|
return userEntity;
|
||||||
}
|
}
|
||||||
LOG.warnf("No account found for request %s", getRequestPath());
|
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
LOG.warnf("Unauthorized request %s", getRequestPath());
|
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getRequestPath()
|
|
||||||
{
|
|
||||||
return String.format("[%s, %s]", request.method().name(), request.path());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/main/java/de/tavolio/Role.java
Normal file
6
src/main/java/de/tavolio/Role.java
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package de.tavolio;
|
||||||
|
|
||||||
|
public enum Role
|
||||||
|
{
|
||||||
|
ROOT, CLIENT, USER
|
||||||
|
}
|
||||||
@ -1,113 +0,0 @@
|
|||||||
package de.tavolio.account;
|
|
||||||
|
|
||||||
import de.tavolio.member.MembershipEntity;
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "account")
|
|
||||||
public class AccountEntity extends PanacheEntityBase
|
|
||||||
{
|
|
||||||
@Id
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
private String firstname;
|
|
||||||
|
|
||||||
private String lastname;
|
|
||||||
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
@Column(name = "account_password")
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private AccountStatus status;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "account")
|
|
||||||
private Set<MembershipEntity> memberships;
|
|
||||||
|
|
||||||
public static AccountEntity init()
|
|
||||||
{
|
|
||||||
return new AccountEntity().setId(UUID.randomUUID().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId()
|
|
||||||
{
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity setId(String id)
|
|
||||||
{
|
|
||||||
this.id = id;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFirstname()
|
|
||||||
{
|
|
||||||
return firstname;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity setFirstname(String firstname)
|
|
||||||
{
|
|
||||||
this.firstname = firstname;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLastname()
|
|
||||||
{
|
|
||||||
return lastname;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity setLastname(String lastname)
|
|
||||||
{
|
|
||||||
this.lastname = lastname;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEmail()
|
|
||||||
{
|
|
||||||
return email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity setEmail(String email)
|
|
||||||
{
|
|
||||||
this.email = email;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPassword()
|
|
||||||
{
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity setPassword(String password)
|
|
||||||
{
|
|
||||||
this.password = password;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<MembershipEntity> getMemberships()
|
|
||||||
{
|
|
||||||
return memberships;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity setMemberships(Set<MembershipEntity> memberships)
|
|
||||||
{
|
|
||||||
this.memberships = memberships;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountStatus getStatus()
|
|
||||||
{
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity setStatus(AccountStatus status)
|
|
||||||
{
|
|
||||||
this.status = status;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
package de.tavolio.account;
|
|
||||||
|
|
||||||
import de.tavolio.account.dto.Account;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class AccountMapper
|
|
||||||
{
|
|
||||||
public Account map(AccountEntity accountEntity)
|
|
||||||
{
|
|
||||||
return new Account(accountEntity.getId(), accountEntity.getFirstname(), accountEntity.getLastname(), accountEntity.getEmail(), accountEntity.getStatus());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package de.tavolio.account;
|
|
||||||
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
|
||||||
import io.quarkus.panache.common.Parameters;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class AccountRepo implements PanacheRepositoryBase<AccountEntity, String>
|
|
||||||
{
|
|
||||||
public Optional<AccountEntity> findOptionalByEmail(String email)
|
|
||||||
{
|
|
||||||
return find("email = :email", Parameters.with("email", email)).firstResultOptional();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
package de.tavolio.account;
|
|
||||||
|
|
||||||
import de.tavolio.account.dto.Account;
|
|
||||||
import de.tavolio.account.dto.AccountCreation;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.POST;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.PathParam;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
|
|
||||||
@Path("/accounts")
|
|
||||||
public class AccountResource
|
|
||||||
{
|
|
||||||
@Inject
|
|
||||||
Logger LOG;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountService accountService;
|
|
||||||
|
|
||||||
@POST
|
|
||||||
public Account post(@Valid AccountCreation account)
|
|
||||||
{
|
|
||||||
Account createdAccount = accountService.create(account);
|
|
||||||
LOG.infof("Created account successfully: %s", account.email());
|
|
||||||
return createdAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/{id}")
|
|
||||||
public Account get(@PathParam("id") String id)
|
|
||||||
{
|
|
||||||
return accountService.getUser(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
package de.tavolio.account;
|
|
||||||
|
|
||||||
import de.tavolio.AuthenticationService;
|
|
||||||
import de.tavolio.account.dto.Account;
|
|
||||||
import de.tavolio.account.dto.AccountCreation;
|
|
||||||
import io.quarkus.elytron.security.common.BcryptUtil;
|
|
||||||
import io.quarkus.security.UnauthorizedException;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class AccountService
|
|
||||||
{
|
|
||||||
@Inject
|
|
||||||
Logger LOG;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountRepo accountRepo;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountMapper accountMapper;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AuthenticationService authenticationService;
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public Account create(AccountCreation account)
|
|
||||||
{
|
|
||||||
AccountEntity accountEntity = AccountEntity.init();
|
|
||||||
accountEntity.setEmail(account.email())
|
|
||||||
.setFirstname(account.firstname())
|
|
||||||
.setLastname(account.lastname())
|
|
||||||
.setPassword(BcryptUtil.bcryptHash(account.password()))
|
|
||||||
.setStatus(AccountStatus.INIT);
|
|
||||||
accountRepo.persist(accountEntity);
|
|
||||||
return accountMapper.map(accountEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Account getUser(String id)
|
|
||||||
{
|
|
||||||
AccountEntity account = authenticationService.requireUser();
|
|
||||||
AccountEntity requestedAccount = accountRepo.findById(id);
|
|
||||||
if (requestedAccount != null && requestedAccount.getId().equals(account.getId()))
|
|
||||||
{
|
|
||||||
return accountMapper.map(authenticationService.requireUser());
|
|
||||||
}
|
|
||||||
LOG.errorf("Cannot access account");
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package de.tavolio.account;
|
|
||||||
|
|
||||||
public enum AccountStatus
|
|
||||||
{
|
|
||||||
INIT, REGISTERED
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package de.tavolio.account.dto;
|
|
||||||
|
|
||||||
import de.tavolio.account.AccountStatus;
|
|
||||||
|
|
||||||
public record Account(String id, String firstname, String lastname, String email, AccountStatus status)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
59
src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java
Normal file
59
src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package de.tavolio.bootstrap;
|
||||||
|
|
||||||
|
import de.tavolio.realm.user.UserEntity;
|
||||||
|
import de.tavolio.realm.user.UserRepo;
|
||||||
|
import de.tavolio.realm.user.UserStatus;
|
||||||
|
import de.tavolio.bootstrap.model.Account;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AccountBootstrapper
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
UserRepo userRepo;
|
||||||
|
|
||||||
|
public void bootstrap(RealmEntity realm, List<Account> accounts)
|
||||||
|
{
|
||||||
|
for (Account account : accounts)
|
||||||
|
{
|
||||||
|
run(realm, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run(RealmEntity realm, Account account)
|
||||||
|
{
|
||||||
|
Optional<UserEntity> existingAccount = userRepo.findOptionalByRealmAndEmail(realm, account.email());
|
||||||
|
if (existingAccount.isEmpty())
|
||||||
|
{
|
||||||
|
UserEntity newAccount = new UserEntity();
|
||||||
|
newAccount.setId(UUID.randomUUID().toString());
|
||||||
|
newAccount.setEmail(account.email());
|
||||||
|
newAccount.setFirstname(account.firstname());
|
||||||
|
newAccount.setLastname(account.lastname());
|
||||||
|
newAccount.setRealm(realm);
|
||||||
|
newAccount.setStatus(UserStatus.INIT);
|
||||||
|
newAccount.setPassword(resolvePassword(account));
|
||||||
|
userRepo.persist(newAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePassword(Account account)
|
||||||
|
{
|
||||||
|
if (account.passwordFromEnv() != null)
|
||||||
|
{
|
||||||
|
return BcryptUtil.bcryptHash(System.getenv(account.passwordFromEnv()));
|
||||||
|
}
|
||||||
|
if (account.passwordPlain() != null)
|
||||||
|
{
|
||||||
|
return BcryptUtil.bcryptHash(account.passwordPlain());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/main/java/de/tavolio/bootstrap/BootstrapService.java
Normal file
40
src/main/java/de/tavolio/bootstrap/BootstrapService.java
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package de.tavolio.bootstrap;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
|
import de.tavolio.bootstrap.model.Bootstrap;
|
||||||
|
import de.tavolio.bootstrap.model.Realm;
|
||||||
|
import io.quarkus.runtime.Startup;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class BootstrapService
|
||||||
|
{
|
||||||
|
private static final Path PATH = Path.of("/home/andreas/Documents/dev/iam-backend/src/main/resources/bootstrap.yaml");
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RealmBootstrapService realmBootstrapService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SuperuserBootstrapper superuserBootstrapper;
|
||||||
|
|
||||||
|
@Startup
|
||||||
|
@Transactional
|
||||||
|
void bootstrap() throws IOException
|
||||||
|
{
|
||||||
|
superuserBootstrapper.bootstrap();
|
||||||
|
Bootstrap bootstrap = OBJECT_MAPPER.readValue(PATH.toFile(), Bootstrap.class);
|
||||||
|
for (Map.Entry<String, Realm> realmEntry : bootstrap.realms().entrySet())
|
||||||
|
{
|
||||||
|
realmBootstrapService.bootstrap(realmEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java
Normal file
30
src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package de.tavolio.bootstrap;
|
||||||
|
|
||||||
|
import de.tavolio.bootstrap.model.Client;
|
||||||
|
import de.tavolio.realm.client.ClientEntity;
|
||||||
|
import de.tavolio.realm.client.ClientRepo;
|
||||||
|
import de.tavolio.realm.client.ClientService;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ClientBootstrapper
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
ClientService clientService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ClientRepo clientRepo;
|
||||||
|
|
||||||
|
public void bootstrap(RealmEntity realm, Map<String, Client> clients)
|
||||||
|
{
|
||||||
|
for (Map.Entry<String, Client> clientEntry : clients.entrySet())
|
||||||
|
{
|
||||||
|
ClientEntity client = clientService.findOrCreate(realm, clientEntry);
|
||||||
|
clientRepo.persist(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/java/de/tavolio/bootstrap/KeyBootstrapper.java
Normal file
22
src/main/java/de/tavolio/bootstrap/KeyBootstrapper.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package de.tavolio.bootstrap;
|
||||||
|
|
||||||
|
import de.tavolio.bootstrap.model.Key;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.key.KeypairService;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class KeyBootstrapper
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
KeypairService keypairService;
|
||||||
|
|
||||||
|
void bootstrap(RealmEntity realm, Key key)
|
||||||
|
{
|
||||||
|
if (realm.getKeys().isEmpty())
|
||||||
|
{
|
||||||
|
keypairService.create(realm, key.type(), key.alg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
package de.tavolio.bootstrap;
|
||||||
|
|
||||||
|
import de.tavolio.bootstrap.model.Audience;
|
||||||
|
import de.tavolio.bootstrap.model.Realm;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmRepo;
|
||||||
|
import de.tavolio.realm.RealmService;
|
||||||
|
import de.tavolio.realm.audience.AudienceStrategyEntity;
|
||||||
|
import de.tavolio.realm.audience.AudienceStrategyRepo;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RealmBootstrapService
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
RealmService realmService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KeyBootstrapper keyBootstrapper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ClientBootstrapper clientBootstrapper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RoleBootstrapper roleBootstrapper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RealmRepo realmRepo;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AccountBootstrapper accountBootstrapper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AudienceStrategyRepo audienceStrategyRepo;
|
||||||
|
|
||||||
|
public void bootstrap(Map.Entry<String, Realm> realmEntry)
|
||||||
|
{
|
||||||
|
Realm realmValue = realmEntry.getValue();
|
||||||
|
RealmEntity realm = run(realmEntry.getKey(), realmEntry.getValue());
|
||||||
|
roleBootstrapper.bootstrap(realm, realmValue.roles());
|
||||||
|
keyBootstrapper.bootstrap(realm, realmValue.key());
|
||||||
|
clientBootstrapper.bootstrap(realm, realmValue.clients());
|
||||||
|
accountBootstrapper.bootstrap(realm, realmValue.accounts());
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity run(String key, Realm realm)
|
||||||
|
{
|
||||||
|
Optional<RealmEntity> existingRealm = realmRepo.findByIdOptional(key);
|
||||||
|
if (existingRealm.isPresent())
|
||||||
|
{
|
||||||
|
return existingRealm.get();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RealmEntity newRealm = new RealmEntity().setKey(key).setName(realm.name());
|
||||||
|
realmRepo.persist(newRealm);
|
||||||
|
bootstrapAudience(newRealm, realm.audience());
|
||||||
|
return newRealm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bootstrapAudience(RealmEntity realm, Audience audience)
|
||||||
|
{
|
||||||
|
if ("static".equals(audience.strategy()))
|
||||||
|
{
|
||||||
|
AudienceStrategyEntity entity = new AudienceStrategyEntity();
|
||||||
|
entity.setId(UUID.randomUUID().toString());
|
||||||
|
entity.setStrategy("static");
|
||||||
|
entity.setValue(audience.value());
|
||||||
|
entity.setRealm(realm);
|
||||||
|
audienceStrategyRepo.persist(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AudienceStrategyEntity entity = new AudienceStrategyEntity();
|
||||||
|
entity.setId(UUID.randomUUID().toString());
|
||||||
|
entity.setStrategy("realm");
|
||||||
|
entity.setRealm(realm);
|
||||||
|
audienceStrategyRepo.persist(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java
Normal file
43
src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package de.tavolio.bootstrap;
|
||||||
|
|
||||||
|
import de.tavolio.bootstrap.model.Role;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.role.RoleEntity;
|
||||||
|
import de.tavolio.realm.role.RoleRepo;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RoleBootstrapper
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
RoleRepo roleRepo;
|
||||||
|
|
||||||
|
public void bootstrap(RealmEntity realm, Map<String, Role> roles)
|
||||||
|
{
|
||||||
|
for (Map.Entry<String, Role> roleEntry : roles.entrySet())
|
||||||
|
{
|
||||||
|
run(realm, roleEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run(RealmEntity realm, Map.Entry<String, Role> roleEntry)
|
||||||
|
{
|
||||||
|
RoleEntity role = getOrCreateRole(realm, roleEntry.getKey());
|
||||||
|
for (String permission : roleEntry.getValue().permissions())
|
||||||
|
{
|
||||||
|
if (!role.hasPermission(permission))
|
||||||
|
{
|
||||||
|
role.getPermissions().add(permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roleRepo.persist(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoleEntity getOrCreateRole(RealmEntity realm, String role)
|
||||||
|
{
|
||||||
|
return roleRepo.findByNameAndRealmOptional(role, realm).orElse(new RoleEntity().setId(UUID.randomUUID().toString()).setName(role).setRealm(realm));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package de.tavolio.bootstrap;
|
||||||
|
|
||||||
|
import de.tavolio.superuser.SuperuserEntity;
|
||||||
|
import de.tavolio.superuser.SuperuserRepo;
|
||||||
|
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.eclipse.microprofile.config.Config;
|
||||||
|
import org.eclipse.microprofile.config.ConfigProvider;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SuperuserBootstrapper
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
SuperuserRepo superuserRepo;
|
||||||
|
|
||||||
|
public void bootstrap()
|
||||||
|
{
|
||||||
|
Config config = ConfigProvider.getConfig();
|
||||||
|
String username = config.getValue("dev.dinauer.idp.superuser.username", String.class);
|
||||||
|
String password = config.getValue("dev.dinauer.idp.superuser.password", String.class);
|
||||||
|
if (!StringUtils.isBlank(username) && !StringUtils.isBlank(password) && superuserRepo.count() == 0)
|
||||||
|
{
|
||||||
|
SuperuserEntity superuser = new SuperuserEntity();
|
||||||
|
superuser.setId(username);
|
||||||
|
superuser.setPassword(BcryptUtil.bcryptHash(password));
|
||||||
|
superuserRepo.persist(superuser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/java/de/tavolio/bootstrap/model/Account.java
Normal file
7
src/main/java/de/tavolio/bootstrap/model/Account.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public record Account(String email, @JsonProperty("first-name") String firstname, @JsonProperty("last-name") String lastname, @JsonProperty("password-env") String passwordFromEnv, @JsonProperty("password-plain") String passwordPlain)
|
||||||
|
{
|
||||||
|
}
|
||||||
5
src/main/java/de/tavolio/bootstrap/model/Audience.java
Normal file
5
src/main/java/de/tavolio/bootstrap/model/Audience.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
|
public record Audience(String strategy, String value)
|
||||||
|
{
|
||||||
|
}
|
||||||
7
src/main/java/de/tavolio/bootstrap/model/Bootstrap.java
Normal file
7
src/main/java/de/tavolio/bootstrap/model/Bootstrap.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record Bootstrap(Map<String, Realm> realms)
|
||||||
|
{
|
||||||
|
}
|
||||||
9
src/main/java/de/tavolio/bootstrap/model/Client.java
Normal file
9
src/main/java/de/tavolio/bootstrap/model/Client.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record Client(@JsonProperty("client-secret") String clientSecret, @JsonProperty("redirect-uri") String redirectURI, List<String> roles)
|
||||||
|
{
|
||||||
|
}
|
||||||
5
src/main/java/de/tavolio/bootstrap/model/Key.java
Normal file
5
src/main/java/de/tavolio/bootstrap/model/Key.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
|
public record Key(String type, String alg)
|
||||||
|
{
|
||||||
|
}
|
||||||
8
src/main/java/de/tavolio/bootstrap/model/Realm.java
Normal file
8
src/main/java/de/tavolio/bootstrap/model/Realm.java
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record Realm(String name, Key key, Audience audience, Map<String, Client> clients, Map<String, Role> roles, List<String> permissions, List<Account> accounts)
|
||||||
|
{
|
||||||
|
}
|
||||||
7
src/main/java/de/tavolio/bootstrap/model/Role.java
Normal file
7
src/main/java/de/tavolio/bootstrap/model/Role.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record Role(List<String> permissions)
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package de.tavolio.member;
|
|
||||||
|
|
||||||
import de.tavolio.AuthenticationService;
|
|
||||||
import de.tavolio.account.AccountEntity;
|
|
||||||
import de.tavolio.member.dto.AccountMemberships;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
@Path("/memberships")
|
|
||||||
public class AccountMembershipResource
|
|
||||||
{
|
|
||||||
@Inject
|
|
||||||
MembershipRepo membershipRepo;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AuthenticationService authenticationService;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
MembershipMapper membershipMapper;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
public AccountMemberships get()
|
|
||||||
{
|
|
||||||
AccountEntity account = authenticationService.requireUser();
|
|
||||||
return new AccountMemberships(
|
|
||||||
membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(TenantType.ORGANISATION, account)),
|
|
||||||
membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(TenantType.RESTAURANT, account))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
package de.tavolio.member;
|
|
||||||
|
|
||||||
import de.tavolio.account.AccountEntity;
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "membership")
|
|
||||||
public class MembershipEntity extends PanacheEntityBase
|
|
||||||
{
|
|
||||||
@Id
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
@Column(name = "tenant_type")
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private TenantType tenantType;
|
|
||||||
|
|
||||||
@Column(name = "tenant_id")
|
|
||||||
private String tenantId;
|
|
||||||
|
|
||||||
@Column(name = "member_role")
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private MembershipRole role;
|
|
||||||
|
|
||||||
@Column(name = "member_since")
|
|
||||||
private ZonedDateTime memberSince;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "account_id")
|
|
||||||
private AccountEntity account;
|
|
||||||
|
|
||||||
public static MembershipEntity init()
|
|
||||||
{
|
|
||||||
return new MembershipEntity().setId(UUID.randomUUID().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId()
|
|
||||||
{
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MembershipEntity setId(String id)
|
|
||||||
{
|
|
||||||
this.id = id;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TenantType getTenantType()
|
|
||||||
{
|
|
||||||
return tenantType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MembershipEntity setTenantType(TenantType tenantType)
|
|
||||||
{
|
|
||||||
this.tenantType = tenantType;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTenantId()
|
|
||||||
{
|
|
||||||
return tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MembershipEntity setTenantId(String tenantId)
|
|
||||||
{
|
|
||||||
this.tenantId = tenantId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MembershipRole getRole()
|
|
||||||
{
|
|
||||||
return role;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MembershipEntity setRole(MembershipRole role)
|
|
||||||
{
|
|
||||||
this.role = role;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ZonedDateTime getMemberSince()
|
|
||||||
{
|
|
||||||
return memberSince;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MembershipEntity setMemberSince(ZonedDateTime memberSince)
|
|
||||||
{
|
|
||||||
this.memberSince = memberSince;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountEntity getAccount()
|
|
||||||
{
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MembershipEntity setAccount(AccountEntity account)
|
|
||||||
{
|
|
||||||
this.account = account;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
package de.tavolio.member;
|
|
||||||
|
|
||||||
import de.tavolio.account.AccountMapper;
|
|
||||||
import de.tavolio.member.dto.Membership;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class MembershipMapper
|
|
||||||
{
|
|
||||||
@Inject
|
|
||||||
AccountMapper accountMapper;
|
|
||||||
|
|
||||||
public Membership map(MembershipEntity membership)
|
|
||||||
{
|
|
||||||
return new Membership(membership.getId(), membership.getTenantType(), membership.getTenantId(), membership.getRole(), accountMapper.map(membership.getAccount()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Membership> map(List<MembershipEntity> memberships)
|
|
||||||
{
|
|
||||||
return memberships.stream().map(this::map).toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
package de.tavolio.member;
|
|
||||||
|
|
||||||
import de.tavolio.account.AccountEntity;
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
|
||||||
import io.quarkus.panache.common.Parameters;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class MembershipRepo implements PanacheRepositoryBase<MembershipEntity, String>
|
|
||||||
{
|
|
||||||
public List<MembershipEntity> findByTenantTypeAndAccount(TenantType tenantType, AccountEntity account)
|
|
||||||
{
|
|
||||||
return list("tenantType = :tenantType AND account = :account", Parameters.with("tenantType", tenantType).and("account", account));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MembershipEntity> findByTenantTypeAndTenantId(TenantType tenantType, String tenantId)
|
|
||||||
{
|
|
||||||
return list("tenantType = :tenantType AND tenantId = :tenantId", Parameters.with("tenantType", tenantType).and("tenantId", tenantId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package de.tavolio.member;
|
|
||||||
|
|
||||||
public enum MembershipRole
|
|
||||||
{
|
|
||||||
OWNER, ADMIN, MEMBER
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
package de.tavolio.member;
|
|
||||||
|
|
||||||
import de.tavolio.AuthenticationService;
|
|
||||||
import de.tavolio.account.AccountEntity;
|
|
||||||
import de.tavolio.account.AccountRepo;
|
|
||||||
import de.tavolio.account.AccountStatus;
|
|
||||||
import de.tavolio.member.dto.Membership;
|
|
||||||
import de.tavolio.member.dto.MembershipCreation;
|
|
||||||
import io.quarkus.security.UnauthorizedException;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import jakarta.ws.rs.BadRequestException;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class MembershipService
|
|
||||||
{
|
|
||||||
@Inject
|
|
||||||
Logger LOG;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AuthenticationService authenticationService;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
MembershipRepo membershipRepo;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
MembershipMapper membershipMapper;
|
|
||||||
@Inject
|
|
||||||
AccountRepo accountRepo;
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public Membership create(TenantType tenantType, String tenantId, MembershipCreation membershipCreation)
|
|
||||||
{
|
|
||||||
switch (tenantType)
|
|
||||||
{
|
|
||||||
case ORGANISATION ->
|
|
||||||
{
|
|
||||||
if (membershipCreation.role().equals(MembershipRole.OWNER))
|
|
||||||
{
|
|
||||||
if (authenticationService.isSuperUser())
|
|
||||||
{
|
|
||||||
AccountEntity account = accountRepo.findById(membershipCreation.accountId());
|
|
||||||
|
|
||||||
MembershipEntity membership = MembershipEntity.init();
|
|
||||||
membership.setAccount(account);
|
|
||||||
membership.setRole(membershipCreation.role());
|
|
||||||
membership.setTenantType(TenantType.ORGANISATION);
|
|
||||||
membership.setTenantId(tenantId);
|
|
||||||
membership.setMemberSince(ZonedDateTime.now());
|
|
||||||
membershipRepo.persist(membership);
|
|
||||||
|
|
||||||
account.setStatus(AccountStatus.REGISTERED);
|
|
||||||
accountRepo.persist(account);
|
|
||||||
|
|
||||||
return membershipMapper.map(membership);
|
|
||||||
}
|
|
||||||
LOG.errorf("Membership with role 'Owner' cannot be created without superuser permissions");
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case RESTAURANT ->
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
default ->
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new BadRequestException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Membership> findByTenantType(TenantType tenantType)
|
|
||||||
{
|
|
||||||
return membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(tenantType, authenticationService.requireUser()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Membership> findByTenantTypeAndTenantId(TenantType tenantType, String tenantId)
|
|
||||||
{
|
|
||||||
return membershipMapper.map(membershipRepo.findByTenantTypeAndTenantId(tenantType, tenantId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
package de.tavolio.member;
|
|
||||||
|
|
||||||
import de.tavolio.member.dto.Membership;
|
|
||||||
import de.tavolio.member.dto.MembershipCreation;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.ws.rs.*;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.jboss.resteasy.reactive.common.NotImplementedYet;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Path("/{tenant-type}")
|
|
||||||
public class TenantMembershipResource
|
|
||||||
{
|
|
||||||
@Inject
|
|
||||||
Logger LOG;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
MembershipService membershipService;
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("/{tenant-id}/memberships")
|
|
||||||
public Membership post(@PathParam("tenant-type") String tenantType, @PathParam("tenant-id") String tenantId, MembershipCreation membershipCreation)
|
|
||||||
{
|
|
||||||
switch (tenantType)
|
|
||||||
{
|
|
||||||
case "organisations" ->
|
|
||||||
{
|
|
||||||
Membership membership = membershipService.create(TenantType.ORGANISATION, tenantId, membershipCreation);
|
|
||||||
LOG.infof("Created membership for organisation %s", tenantId);
|
|
||||||
return membership;
|
|
||||||
}
|
|
||||||
case "restaurants" ->
|
|
||||||
{
|
|
||||||
Membership membership = membershipService.create(TenantType.RESTAURANT, tenantId, membershipCreation);
|
|
||||||
LOG.infof("Created membership for restaurant %s", tenantId);
|
|
||||||
return membership;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new BadRequestException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/{tenant-id}/memberships")
|
|
||||||
public List<Membership> get(@PathParam("tenant-type") String tenantType, @PathParam("tenant-id") String tenantId)
|
|
||||||
{
|
|
||||||
switch (tenantType)
|
|
||||||
{
|
|
||||||
case "organisations" ->
|
|
||||||
{
|
|
||||||
return membershipService.findByTenantTypeAndTenantId(TenantType.ORGANISATION, tenantId);
|
|
||||||
}
|
|
||||||
case "restaurants" ->
|
|
||||||
{
|
|
||||||
throw new NotImplementedYet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG.errorf("Unknown tenant type %s", tenantType);
|
|
||||||
throw new BadRequestException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/memberships")
|
|
||||||
public List<Membership> get(@PathParam("tenant-type") String tenantType)
|
|
||||||
{
|
|
||||||
switch (tenantType)
|
|
||||||
{
|
|
||||||
case "organisations" ->
|
|
||||||
{
|
|
||||||
return membershipService.findByTenantType(TenantType.ORGANISATION);
|
|
||||||
}
|
|
||||||
case "restaurants" ->
|
|
||||||
{
|
|
||||||
return membershipService.findByTenantType(TenantType.RESTAURANT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG.errorf("Unknown tenant type %s", tenantType);
|
|
||||||
throw new BadRequestException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package de.tavolio.member;
|
|
||||||
|
|
||||||
public enum TenantType
|
|
||||||
{
|
|
||||||
ORGANISATION, RESTAURANT
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package de.tavolio.member.dto;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public record AccountMemberships(List<Membership> organisations, List<Membership> restaurants)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
package de.tavolio.member.dto;
|
|
||||||
|
|
||||||
import de.tavolio.account.dto.Account;
|
|
||||||
import de.tavolio.member.MembershipRole;
|
|
||||||
import de.tavolio.member.TenantType;
|
|
||||||
|
|
||||||
public record Membership(String id, TenantType tenantType, String tenantId, MembershipRole role, Account account)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package de.tavolio.member.dto;
|
|
||||||
|
|
||||||
import de.tavolio.member.MembershipRole;
|
|
||||||
|
|
||||||
public record MembershipCreation(String accountId, MembershipRole role)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
30
src/main/java/de/tavolio/oidc/GrantType.java
Normal file
30
src/main/java/de/tavolio/oidc/GrantType.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package de.tavolio.oidc;
|
||||||
|
|
||||||
|
public enum GrantType
|
||||||
|
{
|
||||||
|
AUTH_CODE("authorization_code"),
|
||||||
|
CLIENT_CREDENTIALS("client_credentials");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
GrantType(String value)
|
||||||
|
{
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue()
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GrantType fromValue(String value)
|
||||||
|
{
|
||||||
|
for (GrantType type : GrantType.values())
|
||||||
|
{
|
||||||
|
if (type.value.equalsIgnoreCase(value))
|
||||||
|
{
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
} throw new IllegalArgumentException("Unknown grant type: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main/java/de/tavolio/oidc/IssuerService.java
Normal file
16
src/main/java/de/tavolio/oidc/IssuerService.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package de.tavolio.oidc;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class IssuerService
|
||||||
|
{
|
||||||
|
@ConfigProperty(name = "dev.dinauer.idp.origin")
|
||||||
|
String origin;
|
||||||
|
|
||||||
|
public String getIssuer(String realmKey)
|
||||||
|
{
|
||||||
|
return String.format("%s/api/iam-backend/realms/%s", origin, realmKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/main/java/de/tavolio/oidc/JwksService.java
Normal file
47
src/main/java/de/tavolio/oidc/JwksService.java
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package de.tavolio.oidc;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmRepo;
|
||||||
|
import de.tavolio.realm.key.KeypairEntity;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Path("/realms/{realm-key}/oidc/keys")
|
||||||
|
public class JwksService
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
RealmRepo realmRepo;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Map<String, Object> get(@PathParam("realm-key") String realmKey)
|
||||||
|
{
|
||||||
|
RealmEntity realm = realmRepo.findByKey(realmKey);
|
||||||
|
if (realm != null)
|
||||||
|
{
|
||||||
|
List<Map<String, String>> result = new LinkedList<>();
|
||||||
|
for (KeypairEntity keypair : realm.getKeys())
|
||||||
|
{
|
||||||
|
if ("EC".equals(keypair.getType()))
|
||||||
|
{
|
||||||
|
result.add(Map.ofEntries(
|
||||||
|
Map.entry("kty", "EC"),
|
||||||
|
Map.entry("alg", keypair.getAlg()),
|
||||||
|
Map.entry("use", keypair.getUse()),
|
||||||
|
Map.entry("crv", keypair.getCrv()),
|
||||||
|
Map.entry("kid", keypair.getId()),
|
||||||
|
Map.entry("x", keypair.getX()),
|
||||||
|
Map.entry("y", keypair.getY())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Map.ofEntries(Map.entry("keys", result));
|
||||||
|
}
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/main/java/de/tavolio/oidc/OidcConfiguration.java
Normal file
61
src/main/java/de/tavolio/oidc/OidcConfiguration.java
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package de.tavolio.oidc;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class OidcConfiguration
|
||||||
|
{
|
||||||
|
private String issuer;
|
||||||
|
|
||||||
|
@JsonProperty("token_endpoint")
|
||||||
|
private String tokenEndpoint;
|
||||||
|
|
||||||
|
@JsonProperty("authorization_endpoint")
|
||||||
|
private String authorizationEndpoint;
|
||||||
|
|
||||||
|
@JsonProperty("jwks_uri")
|
||||||
|
private String jwksURI;
|
||||||
|
|
||||||
|
public String getIssuer()
|
||||||
|
{
|
||||||
|
return issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OidcConfiguration setIssuer(String issuer)
|
||||||
|
{
|
||||||
|
this.issuer = issuer;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenEndpoint()
|
||||||
|
{
|
||||||
|
return tokenEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OidcConfiguration setTokenEndpoint(String tokenEndpoint)
|
||||||
|
{
|
||||||
|
this.tokenEndpoint = tokenEndpoint;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorizationEndpoint()
|
||||||
|
{
|
||||||
|
return authorizationEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OidcConfiguration setAuthorizationEndpoint(String authorizationEndpoint)
|
||||||
|
{
|
||||||
|
this.authorizationEndpoint = authorizationEndpoint;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJwksURI()
|
||||||
|
{
|
||||||
|
return jwksURI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OidcConfiguration setJwksURI(String jwksURI)
|
||||||
|
{
|
||||||
|
this.jwksURI = jwksURI;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/java/de/tavolio/oidc/OidcConfigurationResource.java
Normal file
36
src/main/java/de/tavolio/oidc/OidcConfigurationResource.java
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package de.tavolio.oidc;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmRepo;
|
||||||
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
|
@RequestScoped
|
||||||
|
public class OidcConfigurationResource
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
RealmRepo realmRepo;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
IssuerService issuerService;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "dev.dinauer.idp.origin")
|
||||||
|
String origin;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public OidcConfiguration get(@PathParam("realm-key") String realmKey)
|
||||||
|
{
|
||||||
|
RealmEntity realm = realmRepo.findByKey(realmKey);
|
||||||
|
if (realm != null)
|
||||||
|
{
|
||||||
|
return new OidcConfiguration()
|
||||||
|
.setIssuer(issuerService.getIssuer(realmKey))
|
||||||
|
.setTokenEndpoint(String.format("%s/api/iam-backend/realms/%s/protocol/openid-connect/token", origin, realmKey))
|
||||||
|
.setAuthorizationEndpoint(String.format("%s/api/iam-backend/realms/%s/protocol/openid-connect/auth", origin, realmKey))
|
||||||
|
.setJwksURI(String.format("%s/api/iam-backend/realms/%s/protocol/openid-connect/certs", origin, realmKey));
|
||||||
|
}
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/main/java/de/tavolio/oidc/OidcResource.java
Normal file
65
src/main/java/de/tavolio/oidc/OidcResource.java
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package de.tavolio.oidc;
|
||||||
|
|
||||||
|
import de.tavolio.oidc.session.AuthorizationService;
|
||||||
|
import de.tavolio.oidc.session.dto.SessionCreation;
|
||||||
|
import de.tavolio.oidc.token.model.TokenResponse;
|
||||||
|
import de.tavolio.oidc.token.TokenService;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RequestScoped
|
||||||
|
public class OidcResource
|
||||||
|
{
|
||||||
|
@PathParam("realm-key")
|
||||||
|
String realmKey;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JwksService jwksService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
TokenService tokenService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AuthorizationService authorizationService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/certs")
|
||||||
|
public Map<String, Object> certs()
|
||||||
|
{
|
||||||
|
return jwksService.get(realmKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/auth")
|
||||||
|
public Response auth(@QueryParam("client_id") String clientId, @FormParam("email") String email, @FormParam("password") String password)
|
||||||
|
{
|
||||||
|
String code = authorizationService.generateBySessionCreation(realmKey, clientId, new SessionCreation(email, password));
|
||||||
|
return Response.status(302).location(URI.create("http://localhost:8080/callback?code=" + code + "&state=d")).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/token")
|
||||||
|
@RolesAllowed("CLIENT")
|
||||||
|
@Transactional
|
||||||
|
public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("client_id") String clientId, @FormParam("code") String code) throws NoSuchAlgorithmException, InvalidKeySpecException
|
||||||
|
{
|
||||||
|
if (GrantType.AUTH_CODE.equals(GrantType.fromValue(grantType)))
|
||||||
|
{
|
||||||
|
return tokenService.getUserToken(realmKey, code);
|
||||||
|
}
|
||||||
|
if (GrantType.CLIENT_CREDENTIALS.equals(GrantType.fromValue(grantType)))
|
||||||
|
{
|
||||||
|
return tokenService.getClientToken(realmKey);
|
||||||
|
}
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package de.tavolio.oidc.auth;
|
||||||
|
|
||||||
|
import de.tavolio.Role;
|
||||||
|
import de.tavolio.realm.user.UserRepo;
|
||||||
|
import de.tavolio.realm.client.ClientEntity;
|
||||||
|
import de.tavolio.realm.client.ClientRepo;
|
||||||
|
import de.tavolio.superuser.SuperuserEntity;
|
||||||
|
import de.tavolio.superuser.SuperuserRepo;
|
||||||
|
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
|
import io.quarkus.security.AuthenticationFailedException;
|
||||||
|
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||||
|
import io.quarkus.security.identity.IdentityProvider;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
|
||||||
|
import io.quarkus.security.runtime.QuarkusPrincipal;
|
||||||
|
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import io.smallrye.mutiny.infrastructure.Infrastructure;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.context.control.ActivateRequestContext;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class OidcClientIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest>
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
ClientRepo clientRepo;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SuperuserRepo superuserRepo;
|
||||||
|
@Inject
|
||||||
|
UserRepo userRepo;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<UsernamePasswordAuthenticationRequest> getRequestType()
|
||||||
|
{
|
||||||
|
return UsernamePasswordAuthenticationRequest.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@ActivateRequestContext
|
||||||
|
public Uni<SecurityIdentity> authenticate(UsernamePasswordAuthenticationRequest usernamePasswordAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext)
|
||||||
|
{
|
||||||
|
return Uni.createFrom().item(() -> {
|
||||||
|
String username = usernamePasswordAuthenticationRequest.getUsername();
|
||||||
|
String credential = new String(usernamePasswordAuthenticationRequest.getPassword().getPassword());
|
||||||
|
SuperuserEntity superuser = superuserRepo.findById(username);
|
||||||
|
if (superuser != null)
|
||||||
|
{
|
||||||
|
if (BcryptUtil.matches(credential, superuser.getPassword()))
|
||||||
|
{
|
||||||
|
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(username)).addRole(Role.ROOT.toString()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientEntity client = clientRepo.findById(username);
|
||||||
|
if (client != null)
|
||||||
|
{
|
||||||
|
if (BcryptUtil.matches(credential, client.getSecret()))
|
||||||
|
{
|
||||||
|
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(username)).addRole(Role.CLIENT.toString()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AuthenticationFailedException();
|
||||||
|
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package de.tavolio.oidc.session;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmService;
|
||||||
|
import de.tavolio.realm.user.UserEntity;
|
||||||
|
import de.tavolio.realm.user.UserRepo;
|
||||||
|
import de.tavolio.realm.client.ClientEntity;
|
||||||
|
import de.tavolio.realm.client.ClientService;
|
||||||
|
import de.tavolio.realm.code.CodeEntity;
|
||||||
|
import de.tavolio.realm.code.CodeRepo;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.oidc.session.dto.SessionCreation;
|
||||||
|
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuthorizationService
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
UserRepo userRepo;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CodeRepo codeRepo;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RealmService realmService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ClientService clientService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public String generateBySessionCreation(String realmKey, String clientId, SessionCreation sessionCreation)
|
||||||
|
{
|
||||||
|
|
||||||
|
RealmEntity realm = realmService.requireByKey(realmKey);
|
||||||
|
ClientEntity client = clientService.findByIdAndRealm(clientId, realm);
|
||||||
|
Optional<UserEntity> accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, sessionCreation.email());
|
||||||
|
if (accountEntityOptional.isPresent())
|
||||||
|
{
|
||||||
|
UserEntity userEntity = accountEntityOptional.get();
|
||||||
|
if (BcryptUtil.matches(sessionCreation.password(), userEntity.getPassword()))
|
||||||
|
{
|
||||||
|
CodeEntity code = new CodeEntity().setId(UUID.randomUUID().toString());
|
||||||
|
code.setExpiresAt(ZonedDateTime.now().plusMinutes(1));
|
||||||
|
code.setAccount(userEntity);
|
||||||
|
code.setRealm(realm);
|
||||||
|
code.setClient(client);
|
||||||
|
codeRepo.persist(code);
|
||||||
|
return code.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package de.tavolio.session.dto;
|
package de.tavolio.oidc.session.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Email;
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
39
src/main/java/de/tavolio/oidc/token/TokenGenerator.java
Normal file
39
src/main/java/de/tavolio/oidc/token/TokenGenerator.java
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package de.tavolio.oidc.token;
|
||||||
|
|
||||||
|
import de.tavolio.oidc.IssuerService;
|
||||||
|
import io.smallrye.jwt.build.Jwt;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class TokenGenerator
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
IssuerService issuerService;
|
||||||
|
|
||||||
|
public String generateAccessToken(String realmKey, String clientId, String upn, ZonedDateTime expiresAt, PrivateKey key, String keyId)
|
||||||
|
{
|
||||||
|
return Jwt.claims()
|
||||||
|
.upn(upn)
|
||||||
|
.audience(clientId)
|
||||||
|
.claim("realm_key", realmKey)
|
||||||
|
.claim("client_id", clientId)
|
||||||
|
.expiresAt(expiresAt.toInstant())
|
||||||
|
.issuer(issuerService.getIssuer(realmKey)).jws().keyId(keyId)
|
||||||
|
.sign(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateIDToken(String realmKey, String clientId, String upn, ZonedDateTime expiresAt, PrivateKey key, String keyId)
|
||||||
|
{
|
||||||
|
return Jwt.claims()
|
||||||
|
.upn(upn)
|
||||||
|
.claim("realm_key", realmKey)
|
||||||
|
.claim("client_id", clientId)
|
||||||
|
.expiresAt(expiresAt.toInstant())
|
||||||
|
.issuer(issuerService.getIssuer(realmKey)).jws().keyId(keyId)
|
||||||
|
.sign(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/main/java/de/tavolio/oidc/token/TokenService.java
Normal file
75
src/main/java/de/tavolio/oidc/token/TokenService.java
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package de.tavolio.oidc.token;
|
||||||
|
|
||||||
|
import de.tavolio.oidc.token.model.TokenResponse;
|
||||||
|
import de.tavolio.realm.RealmRepo;
|
||||||
|
import de.tavolio.realm.code.CodeEntity;
|
||||||
|
import de.tavolio.realm.code.CodeRepo;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmService;
|
||||||
|
import de.tavolio.realm.key.KeypairEntity;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.Context;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
|
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class TokenService
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
CodeRepo codeRepo;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
TokenGenerator tokenGenerator;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity identity;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RealmRepo realmRepo;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public TokenResponse getClientToken(String realmKey)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse getUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException
|
||||||
|
{
|
||||||
|
return generateUserToken(realmKey, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenResponse generateUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException
|
||||||
|
{
|
||||||
|
String principal = identity.getPrincipal().getName();
|
||||||
|
RealmEntity realm = realmRepo.findById(realmKey);
|
||||||
|
CodeEntity entity = codeRepo.findByRealmAndId(realm, code);
|
||||||
|
if (entity != null && !ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getId()))
|
||||||
|
{
|
||||||
|
KeypairEntity keypair = realm.getKeys().getFirst();
|
||||||
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keypair.getPrivateKey());
|
||||||
|
ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1);
|
||||||
|
PrivateKey signingKey = KeyFactory.getInstance(RealmService.EC).generatePrivate(spec);
|
||||||
|
TokenResponse response = new TokenResponse()
|
||||||
|
.setAccessToken(tokenGenerator.generateAccessToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId()))
|
||||||
|
.setRefreshToken(UUID.randomUUID().toString())
|
||||||
|
.setIdToken(tokenGenerator.generateIDToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId()))
|
||||||
|
.setTokenType("Bearer")
|
||||||
|
.setExpiresAt(expiresAt.toInstant().getEpochSecond());
|
||||||
|
codeRepo.delete(entity);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/main/java/de/tavolio/oidc/token/model/TokenResponse.java
Normal file
76
src/main/java/de/tavolio/oidc/token/model/TokenResponse.java
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package de.tavolio.oidc.token.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class TokenResponse
|
||||||
|
{
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
@JsonProperty("refresh_token")
|
||||||
|
private String refreshToken;
|
||||||
|
|
||||||
|
@JsonProperty("id_token")
|
||||||
|
private String idToken;
|
||||||
|
|
||||||
|
@JsonProperty("token_type")
|
||||||
|
private String tokenType;
|
||||||
|
|
||||||
|
@JsonProperty("expires_at")
|
||||||
|
private Long expiresAt;
|
||||||
|
|
||||||
|
public String getAccessToken()
|
||||||
|
{
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse setAccessToken(String accessToken)
|
||||||
|
{
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRefreshToken()
|
||||||
|
{
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse setRefreshToken(String refreshToken)
|
||||||
|
{
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdToken()
|
||||||
|
{
|
||||||
|
return idToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse setIdToken(String idToken)
|
||||||
|
{
|
||||||
|
this.idToken = idToken;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenType()
|
||||||
|
{
|
||||||
|
return tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse setTokenType(String tokenType)
|
||||||
|
{
|
||||||
|
this.tokenType = tokenType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getExpiresAt()
|
||||||
|
{
|
||||||
|
return expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse setExpiresAt(Long expiresAt)
|
||||||
|
{
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/main/java/de/tavolio/realm/RealmCreation.java
Normal file
5
src/main/java/de/tavolio/realm/RealmCreation.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.tavolio.realm;
|
||||||
|
|
||||||
|
public record RealmCreation(String name, String key)
|
||||||
|
{
|
||||||
|
}
|
||||||
129
src/main/java/de/tavolio/realm/RealmEntity.java
Normal file
129
src/main/java/de/tavolio/realm/RealmEntity.java
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package de.tavolio.realm;
|
||||||
|
|
||||||
|
import de.tavolio.realm.audience.AudienceStrategyEntity;
|
||||||
|
import de.tavolio.realm.key.KeypairEntity;
|
||||||
|
import de.tavolio.realm.user.UserEntity;
|
||||||
|
import de.tavolio.realm.client.ClientEntity;
|
||||||
|
import de.tavolio.realm.code.CodeEntity;
|
||||||
|
import de.tavolio.realm.role.RoleEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "realm")
|
||||||
|
public class RealmEntity
|
||||||
|
{
|
||||||
|
@Id
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
@Column(name = "realm_name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "realm", cascade = CascadeType.ALL)
|
||||||
|
private List<KeypairEntity> keys = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "realm")
|
||||||
|
private List<UserEntity> accounts = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "realm")
|
||||||
|
private List<ClientEntity> clients = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "realm")
|
||||||
|
private List<CodeEntity> codes = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "realm")
|
||||||
|
private List<RoleEntity> roles = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToOne(mappedBy = "realm")
|
||||||
|
private AudienceStrategyEntity audienceStrategy;
|
||||||
|
|
||||||
|
public String getKey()
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity setKey(String key)
|
||||||
|
{
|
||||||
|
this.key = key;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity setName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<KeypairEntity> getKeys()
|
||||||
|
{
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity setKeys(List<KeypairEntity> keys)
|
||||||
|
{
|
||||||
|
this.keys = keys;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserEntity> getAccounts()
|
||||||
|
{
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity setAccounts(List<UserEntity> accounts)
|
||||||
|
{
|
||||||
|
this.accounts = accounts;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClientEntity> getClients()
|
||||||
|
{
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity setClients(List<ClientEntity> clients)
|
||||||
|
{
|
||||||
|
this.clients = clients;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CodeEntity> getCodes()
|
||||||
|
{
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity setCodes(List<CodeEntity> codes)
|
||||||
|
{
|
||||||
|
this.codes = codes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RoleEntity> getRoles()
|
||||||
|
{
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity setRoles(List<RoleEntity> roles)
|
||||||
|
{
|
||||||
|
this.roles = roles;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudienceStrategyEntity getAudienceStrategy()
|
||||||
|
{
|
||||||
|
return audienceStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity setAudienceStrategy(AudienceStrategyEntity audienceStrategy)
|
||||||
|
{
|
||||||
|
this.audienceStrategy = audienceStrategy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/java/de/tavolio/realm/RealmRepo.java
Normal file
21
src/main/java/de/tavolio/realm/RealmRepo.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package de.tavolio.realm;
|
||||||
|
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import io.quarkus.panache.common.Parameters;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RealmRepo implements PanacheRepositoryBase<RealmEntity, String>
|
||||||
|
{
|
||||||
|
public Optional<RealmEntity> findByKeyOptional(String key)
|
||||||
|
{
|
||||||
|
return find("key = :key", Parameters.with("key", key)).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity findByKey(String key)
|
||||||
|
{
|
||||||
|
return find("key = :key", Parameters.with("key", key)).firstResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/main/java/de/tavolio/realm/RealmResource.java
Normal file
29
src/main/java/de/tavolio/realm/RealmResource.java
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package de.tavolio.realm;
|
||||||
|
|
||||||
|
import de.tavolio.oidc.OidcConfigurationResource;
|
||||||
|
import de.tavolio.oidc.OidcResource;
|
||||||
|
import de.tavolio.realm.user.UserResource;
|
||||||
|
import jakarta.enterprise.inject.spi.CDI;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
|
||||||
|
@Path("/realms/{realm-key}")
|
||||||
|
public class RealmResource
|
||||||
|
{
|
||||||
|
@Path("/accounts")
|
||||||
|
public UserResource accounts()
|
||||||
|
{
|
||||||
|
return CDI.current().select(UserResource.class).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/protocol/openid-connect")
|
||||||
|
public OidcResource oidc()
|
||||||
|
{
|
||||||
|
return CDI.current().select(OidcResource.class).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/.well-known/openid-configuration")
|
||||||
|
public OidcConfigurationResource oidcConfigurationResource()
|
||||||
|
{
|
||||||
|
return CDI.current().select(OidcConfigurationResource.class).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/main/java/de/tavolio/realm/RealmScoped.java
Normal file
6
src/main/java/de/tavolio/realm/RealmScoped.java
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package de.tavolio.realm;
|
||||||
|
|
||||||
|
public interface RealmScoped
|
||||||
|
{
|
||||||
|
RealmEntity getRealm();
|
||||||
|
}
|
||||||
44
src/main/java/de/tavolio/realm/RealmService.java
Normal file
44
src/main/java/de/tavolio/realm/RealmService.java
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package de.tavolio.realm;
|
||||||
|
|
||||||
|
import de.tavolio.bootstrap.model.Realm;
|
||||||
|
import de.tavolio.realm.key.KeypairEntity;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.security.spec.ECGenParameterSpec;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RealmService
|
||||||
|
{
|
||||||
|
public static final String EC = "EC";
|
||||||
|
public static final String P256 = "secp256r1";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RealmRepo repo;
|
||||||
|
|
||||||
|
public RealmEntity requireByKey(String key)
|
||||||
|
{
|
||||||
|
RealmEntity realm = repo.findById(key);
|
||||||
|
if (realm != null)
|
||||||
|
{
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RealmEntity create(RealmCreation creation)
|
||||||
|
{
|
||||||
|
RealmEntity realm = new RealmEntity();
|
||||||
|
realm.setKey(creation.key());
|
||||||
|
realm.setName(creation.name());
|
||||||
|
repo.persist(realm);
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package de.tavolio.realm.audience;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmScoped;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "audience_strategy")
|
||||||
|
public class AudienceStrategyEntity implements RealmScoped
|
||||||
|
{
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String strategy;
|
||||||
|
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
@OneToOne
|
||||||
|
@JoinColumn(name = "realm_id")
|
||||||
|
private RealmEntity realm;
|
||||||
|
|
||||||
|
public String getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudienceStrategyEntity setId(String id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStrategy()
|
||||||
|
{
|
||||||
|
return strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudienceStrategyEntity setStrategy(String strategy)
|
||||||
|
{
|
||||||
|
this.strategy = strategy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue()
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudienceStrategyEntity setValue(String value)
|
||||||
|
{
|
||||||
|
this.value = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RealmEntity getRealm()
|
||||||
|
{
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudienceStrategyEntity setRealm(RealmEntity realm)
|
||||||
|
{
|
||||||
|
this.realm = realm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package de.tavolio.realm.audience;
|
||||||
|
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AudienceStrategyRepo implements PanacheRepositoryBase<AudienceStrategyEntity, String>
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package de.tavolio.realm.client;
|
||||||
|
|
||||||
|
public record ClientCreation(String id, String secret)
|
||||||
|
{
|
||||||
|
}
|
||||||
83
src/main/java/de/tavolio/realm/client/ClientEntity.java
Normal file
83
src/main/java/de/tavolio/realm/client/ClientEntity.java
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package de.tavolio.realm.client;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmScoped;
|
||||||
|
import de.tavolio.realm.code.CodeEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "client")
|
||||||
|
public class ClientEntity implements RealmScoped
|
||||||
|
{
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@Column(name = "redirect_uri")
|
||||||
|
private String redirectURI;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "realm_id")
|
||||||
|
private RealmEntity realm;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "client")
|
||||||
|
private List<CodeEntity> codes;
|
||||||
|
|
||||||
|
public String getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity setId(String id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSecret()
|
||||||
|
{
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity setSecret(String secret)
|
||||||
|
{
|
||||||
|
this.secret = secret;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity getRealm()
|
||||||
|
{
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity setRealm(RealmEntity realm)
|
||||||
|
{
|
||||||
|
this.realm = realm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRedirectURI()
|
||||||
|
{
|
||||||
|
return redirectURI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity setRedirectURI(String redirectURI)
|
||||||
|
{
|
||||||
|
this.redirectURI = redirectURI;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CodeEntity> getCodes()
|
||||||
|
{
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity setCodes(List<CodeEntity> codes)
|
||||||
|
{
|
||||||
|
this.codes = codes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/java/de/tavolio/realm/client/ClientRepo.java
Normal file
22
src/main/java/de/tavolio/realm/client/ClientRepo.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package de.tavolio.realm.client;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import io.quarkus.panache.common.Parameters;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ClientRepo implements PanacheRepositoryBase<ClientEntity, String>
|
||||||
|
{
|
||||||
|
public Optional<ClientEntity> findByRealmAndIdOptional(RealmEntity realm, String id)
|
||||||
|
{
|
||||||
|
return find("realm = :realm AND id = :id", Parameters.with("realm", realm).and("id", id)).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity findByRealmAndId(RealmEntity realm, String id)
|
||||||
|
{
|
||||||
|
return find("realm = :realm AND id = :id", Parameters.with("realm", realm).and("id", id)).firstResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/main/java/de/tavolio/realm/client/ClientService.java
Normal file
59
src/main/java/de/tavolio/realm/client/ClientService.java
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package de.tavolio.realm.client;
|
||||||
|
|
||||||
|
import de.tavolio.bootstrap.model.Client;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmRepo;
|
||||||
|
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ClientService
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
ClientRepo clientRepo;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RealmRepo realmRepo;
|
||||||
|
|
||||||
|
public ClientEntity findOrCreate(RealmEntity realm, Map.Entry<String, Client> bootstrap)
|
||||||
|
{
|
||||||
|
String secret = null;
|
||||||
|
if (!StringUtils.isBlank(bootstrap.getValue().clientSecret()))
|
||||||
|
{
|
||||||
|
secret = BcryptUtil.bcryptHash(System.getenv(bootstrap.getValue().clientSecret()));
|
||||||
|
}
|
||||||
|
return clientRepo.findByRealmAndIdOptional(realm, bootstrap.getKey()).orElse(new ClientEntity().setId(bootstrap.getKey()).setSecret(secret).setRedirectURI(bootstrap.getValue().redirectURI()).setRealm(realm));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity findByIdAndRealm(String clientId, RealmEntity realm)
|
||||||
|
{
|
||||||
|
ClientEntity client = clientRepo.findByRealmAndId(realm, clientId);
|
||||||
|
if (client != null)
|
||||||
|
{
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ClientEntity create(String realmKey, ClientCreation client)
|
||||||
|
{
|
||||||
|
RealmEntity realm = realmRepo.findByKey(realmKey);
|
||||||
|
if (realm != null)
|
||||||
|
{
|
||||||
|
ClientEntity entity = new ClientEntity();
|
||||||
|
entity.setId(client.id());
|
||||||
|
entity.setSecret(BcryptUtil.bcryptHash(client.secret()));
|
||||||
|
entity.setRealm(realm);
|
||||||
|
clientRepo.persist(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/main/java/de/tavolio/realm/code/CodeEntity.java
Normal file
86
src/main/java/de/tavolio/realm/code/CodeEntity.java
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package de.tavolio.realm.code;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmScoped;
|
||||||
|
import de.tavolio.realm.user.UserEntity;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.client.ClientEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "code")
|
||||||
|
public class CodeEntity implements RealmScoped
|
||||||
|
{
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private ZonedDateTime expiresAt;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "account_id")
|
||||||
|
private UserEntity account;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "realm_id")
|
||||||
|
private RealmEntity realm;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id")
|
||||||
|
private ClientEntity client;
|
||||||
|
|
||||||
|
public String getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodeEntity setId(String id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZonedDateTime getExpiresAt()
|
||||||
|
{
|
||||||
|
return expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodeEntity setExpiresAt(ZonedDateTime expiresAt)
|
||||||
|
{
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity getAccount()
|
||||||
|
{
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodeEntity setAccount(UserEntity account)
|
||||||
|
{
|
||||||
|
this.account = account;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity getRealm()
|
||||||
|
{
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodeEntity setRealm(RealmEntity realm)
|
||||||
|
{
|
||||||
|
this.realm = realm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity getClient()
|
||||||
|
{
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodeEntity setClient(ClientEntity client)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/java/de/tavolio/realm/code/CodeRepo.java
Normal file
15
src/main/java/de/tavolio/realm/code/CodeRepo.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package de.tavolio.realm.code;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import io.quarkus.panache.common.Parameters;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class CodeRepo implements PanacheRepositoryBase<CodeEntity, String>
|
||||||
|
{
|
||||||
|
public CodeEntity findByRealmAndId(RealmEntity realm, String id)
|
||||||
|
{
|
||||||
|
return find("realm = :realm AND id = :id", Parameters.with("realm", realm).and("id", id)).firstResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/main/java/de/tavolio/realm/key/KeypairEntity.java
Normal file
131
src/main/java/de/tavolio/realm/key/KeypairEntity.java
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package de.tavolio.realm.key;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmScoped;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "keypair")
|
||||||
|
public class KeypairEntity implements RealmScoped
|
||||||
|
{
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "bytea")
|
||||||
|
private byte[] privateKey;
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
private String use;
|
||||||
|
|
||||||
|
private String alg;
|
||||||
|
|
||||||
|
private String crv;
|
||||||
|
|
||||||
|
private String x;
|
||||||
|
|
||||||
|
private String y;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "realm_id")
|
||||||
|
private RealmEntity realm;
|
||||||
|
|
||||||
|
public String getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setId(String id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPrivateKey()
|
||||||
|
{
|
||||||
|
return privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setPrivateKey(byte[] privateKey)
|
||||||
|
{
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity getRealm()
|
||||||
|
{
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setRealm(RealmEntity realm)
|
||||||
|
{
|
||||||
|
this.realm = realm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType()
|
||||||
|
{
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setType(String type)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUse()
|
||||||
|
{
|
||||||
|
return use;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setUse(String use)
|
||||||
|
{
|
||||||
|
this.use = use;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlg()
|
||||||
|
{
|
||||||
|
return alg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setAlg(String alg)
|
||||||
|
{
|
||||||
|
this.alg = alg;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCrv()
|
||||||
|
{
|
||||||
|
return crv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setCrv(String crv)
|
||||||
|
{
|
||||||
|
this.crv = crv;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getX()
|
||||||
|
{
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setX(String x)
|
||||||
|
{
|
||||||
|
this.x = x;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getY()
|
||||||
|
{
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeypairEntity setY(String y)
|
||||||
|
{
|
||||||
|
this.y = y;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/java/de/tavolio/realm/key/KeypairRepo.java
Normal file
9
src/main/java/de/tavolio/realm/key/KeypairRepo.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package de.tavolio.realm.key;
|
||||||
|
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class KeypairRepo implements PanacheRepositoryBase<KeypairEntity, String>
|
||||||
|
{
|
||||||
|
}
|
||||||
82
src/main/java/de/tavolio/realm/key/KeypairService.java
Normal file
82
src/main/java/de/tavolio/realm/key/KeypairService.java
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package de.tavolio.realm.key;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.security.spec.ECGenParameterSpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class KeypairService
|
||||||
|
{
|
||||||
|
public static final String EC = "EC";
|
||||||
|
public static final String P256 = "secp256r1";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KeypairRepo keypairRepo;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void create(RealmEntity realm, String type, String alg)
|
||||||
|
{
|
||||||
|
if ("EC".equals(type))
|
||||||
|
{
|
||||||
|
KeypairEntity keypair = getKeypair(realm);
|
||||||
|
keypair.setRealm(realm);
|
||||||
|
realm.getKeys().add(keypair);
|
||||||
|
keypairRepo.persist(keypair);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private KeypairEntity getKeypair(RealmEntity realm)
|
||||||
|
{
|
||||||
|
KeyPair pair = generate();
|
||||||
|
ECPublicKey publicKey = (ECPublicKey) pair.getPublic();
|
||||||
|
byte[] xBytes = toFixedLength(publicKey.getW().getAffineX().toByteArray());
|
||||||
|
byte[] yBytes = toFixedLength(publicKey.getW().getAffineY().toByteArray());
|
||||||
|
return new KeypairEntity()
|
||||||
|
.setId(UUID.randomUUID().toString())
|
||||||
|
.setRealm(realm)
|
||||||
|
.setPrivateKey(pair.getPrivate().getEncoded())
|
||||||
|
.setType("EC")
|
||||||
|
.setUse("sig")
|
||||||
|
.setAlg("ES256")
|
||||||
|
.setCrv("P-256")
|
||||||
|
.setX(Base64.getUrlEncoder().withoutPadding().encodeToString(xBytes))
|
||||||
|
.setY(Base64.getUrlEncoder().withoutPadding().encodeToString(yBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyPair generate()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
KeyPairGenerator generator = KeyPairGenerator.getInstance(EC);
|
||||||
|
generator.initialize(new ECGenParameterSpec(P256));
|
||||||
|
return generator.generateKeyPair();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] toFixedLength(byte[] input)
|
||||||
|
{
|
||||||
|
if (input.length > 32)
|
||||||
|
{
|
||||||
|
byte[] result = new byte[32];
|
||||||
|
System.arraycopy(input, input.length - 32, result, 0, 32);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/main/java/de/tavolio/realm/role/RoleEntity.java
Normal file
87
src/main/java/de/tavolio/realm/role/RoleEntity.java
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package de.tavolio.realm.role;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmScoped;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "role")
|
||||||
|
public class RoleEntity implements RealmScoped
|
||||||
|
{
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column(name = "role_name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "realm_id")
|
||||||
|
private RealmEntity realm;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(
|
||||||
|
name = "permission",
|
||||||
|
joinColumns = @JoinColumn(name = "role_id")
|
||||||
|
)
|
||||||
|
@Column(name = "permission")
|
||||||
|
private List<String> permissions = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoleEntity setId(String id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoleEntity setName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getPermissions()
|
||||||
|
{
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoleEntity setPermissions(List<String> permissions)
|
||||||
|
{
|
||||||
|
this.permissions = permissions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity getRealm()
|
||||||
|
{
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoleEntity setRealm(RealmEntity realm)
|
||||||
|
{
|
||||||
|
this.realm = realm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPermission(String permission)
|
||||||
|
{
|
||||||
|
for (String value : permissions)
|
||||||
|
{
|
||||||
|
if (permission.equals(value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/java/de/tavolio/realm/role/RoleRepo.java
Normal file
17
src/main/java/de/tavolio/realm/role/RoleRepo.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package de.tavolio.realm.role;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import io.quarkus.panache.common.Parameters;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RoleRepo implements PanacheRepositoryBase<RoleEntity, String>
|
||||||
|
{
|
||||||
|
public Optional<RoleEntity> findByNameAndRealmOptional(String name, RealmEntity realm)
|
||||||
|
{
|
||||||
|
return find("realm = :realm AND name = :name", Parameters.with("realm", realm).and("name", name)).firstResultOptional();
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/main/java/de/tavolio/realm/user/UserEntity.java
Normal file
129
src/main/java/de/tavolio/realm/user/UserEntity.java
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package de.tavolio.realm.user;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmScoped;
|
||||||
|
import de.tavolio.realm.code.CodeEntity;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "user_regular")
|
||||||
|
public class UserEntity implements RealmScoped
|
||||||
|
{
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String firstname;
|
||||||
|
|
||||||
|
private String lastname;
|
||||||
|
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(name = "account_password")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private UserStatus status;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "realm_id")
|
||||||
|
private RealmEntity realm;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "account")
|
||||||
|
private List<CodeEntity> codes;
|
||||||
|
|
||||||
|
public static UserEntity init()
|
||||||
|
{
|
||||||
|
return new UserEntity().setId(UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity setId(String id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstname()
|
||||||
|
{
|
||||||
|
return firstname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity setFirstname(String firstname)
|
||||||
|
{
|
||||||
|
this.firstname = firstname;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastname()
|
||||||
|
{
|
||||||
|
return lastname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity setLastname(String lastname)
|
||||||
|
{
|
||||||
|
this.lastname = lastname;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail()
|
||||||
|
{
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity setEmail(String email)
|
||||||
|
{
|
||||||
|
this.email = email;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword()
|
||||||
|
{
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity setPassword(String password)
|
||||||
|
{
|
||||||
|
this.password = password;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserStatus getStatus()
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity setStatus(UserStatus status)
|
||||||
|
{
|
||||||
|
this.status = status;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmEntity getRealm()
|
||||||
|
{
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity setRealm(RealmEntity realm)
|
||||||
|
{
|
||||||
|
this.realm = realm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CodeEntity> getCodes()
|
||||||
|
{
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEntity setCodes(List<CodeEntity> codes)
|
||||||
|
{
|
||||||
|
this.codes = codes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/java/de/tavolio/realm/user/UserMapper.java
Normal file
20
src/main/java/de/tavolio/realm/user/UserMapper.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package de.tavolio.realm.user;
|
||||||
|
|
||||||
|
import de.tavolio.realm.user.dto.User;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class UserMapper
|
||||||
|
{
|
||||||
|
public List<User> map(List<UserEntity> accountEntities)
|
||||||
|
{
|
||||||
|
return accountEntities.stream().map(this::map).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User map(UserEntity userEntity)
|
||||||
|
{
|
||||||
|
return new User(userEntity.getId(), userEntity.getFirstname(), userEntity.getLastname(), userEntity.getEmail(), userEntity.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/tavolio/realm/user/UserRepo.java
Normal file
23
src/main/java/de/tavolio/realm/user/UserRepo.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package de.tavolio.realm.user;
|
||||||
|
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import io.quarkus.panache.common.Parameters;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class UserRepo implements PanacheRepositoryBase<UserEntity, String>
|
||||||
|
{
|
||||||
|
public Optional<UserEntity> findOptionalByRealmAndEmail(RealmEntity realm, String email)
|
||||||
|
{
|
||||||
|
return find("realm = :realm AND email = :email", Parameters.with("realm", realm).and("email", email)).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserEntity> findByIds(List<String> ids)
|
||||||
|
{
|
||||||
|
return list("id IN :ids", Parameters.with("ids", ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/main/java/de/tavolio/realm/user/UserResource.java
Normal file
53
src/main/java/de/tavolio/realm/user/UserResource.java
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package de.tavolio.realm.user;
|
||||||
|
|
||||||
|
import de.tavolio.realm.user.dto.User;
|
||||||
|
import de.tavolio.realm.user.dto.UserCreation;
|
||||||
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RequestScoped
|
||||||
|
public class UserResource
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
Logger LOG;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UserService userService;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public User post(@Valid UserCreation account)
|
||||||
|
{
|
||||||
|
User createdUser = userService.create("", account);
|
||||||
|
LOG.infof("Created account successfully: %s", account.email());
|
||||||
|
return createdUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public User get(@PathParam("id") String id)
|
||||||
|
{
|
||||||
|
return userService.getUser(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
public User getById(@PathParam("id") String id)
|
||||||
|
{
|
||||||
|
return userService.getUser(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/search")
|
||||||
|
public Map<String, User> get(List<String> ids)
|
||||||
|
{
|
||||||
|
return userService.findByIds(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/main/java/de/tavolio/realm/user/UserService.java
Normal file
83
src/main/java/de/tavolio/realm/user/UserService.java
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package de.tavolio.realm.user;
|
||||||
|
|
||||||
|
import de.tavolio.AuthenticationService;
|
||||||
|
import de.tavolio.realm.user.dto.User;
|
||||||
|
import de.tavolio.realm.user.dto.UserCreation;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.realm.RealmRepo;
|
||||||
|
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
|
import io.quarkus.security.UnauthorizedException;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class UserService
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
Logger LOG;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UserRepo userRepo;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UserMapper userMapper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AuthenticationService authenticationService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RealmRepo realmRepo;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User create(String realmId, UserCreation account)
|
||||||
|
{
|
||||||
|
RealmEntity realm = realmRepo.findByKey(realmId);
|
||||||
|
if (realm != null)
|
||||||
|
{
|
||||||
|
UserEntity userEntity = UserEntity.init();
|
||||||
|
userEntity.setEmail(account.email())
|
||||||
|
.setFirstname(account.firstname())
|
||||||
|
.setLastname(account.lastname())
|
||||||
|
.setPassword(BcryptUtil.bcryptHash(account.password()))
|
||||||
|
.setRealm(realm)
|
||||||
|
.setStatus(UserStatus.INIT);
|
||||||
|
userRepo.persist(userEntity);
|
||||||
|
return userMapper.map(userEntity);
|
||||||
|
}
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<User> get()
|
||||||
|
{
|
||||||
|
return userMapper.map(userRepo.listAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser(String id)
|
||||||
|
{
|
||||||
|
UserEntity account = authenticationService.requireUser();
|
||||||
|
UserEntity requestedAccount = userRepo.findById(id);
|
||||||
|
if (requestedAccount != null && requestedAccount.getId().equals(account.getId()))
|
||||||
|
{
|
||||||
|
return userMapper.map(authenticationService.requireUser());
|
||||||
|
}
|
||||||
|
LOG.errorf("Cannot access account");
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, User> findByIds(List<String> ids)
|
||||||
|
{
|
||||||
|
Map<String, User> accounts = new HashMap<>();
|
||||||
|
for (UserEntity userEntity : userRepo.findByIds(ids))
|
||||||
|
{
|
||||||
|
accounts.put(userEntity.getId(), userMapper.map(userEntity));
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/main/java/de/tavolio/realm/user/UserStatus.java
Normal file
6
src/main/java/de/tavolio/realm/user/UserStatus.java
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package de.tavolio.realm.user;
|
||||||
|
|
||||||
|
public enum UserStatus
|
||||||
|
{
|
||||||
|
INIT, REGISTERED
|
||||||
|
}
|
||||||
7
src/main/java/de/tavolio/realm/user/dto/User.java
Normal file
7
src/main/java/de/tavolio/realm/user/dto/User.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package de.tavolio.realm.user.dto;
|
||||||
|
|
||||||
|
import de.tavolio.realm.user.UserStatus;
|
||||||
|
|
||||||
|
public record User(String id, String firstname, String lastname, String email, UserStatus status)
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
package de.tavolio.account.dto;
|
package de.tavolio.realm.user.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Email;
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
public record AccountCreation(
|
public record UserCreation(
|
||||||
@NotBlank String firstname,
|
@NotBlank String firstname,
|
||||||
@NotBlank String lastname,
|
@NotBlank String lastname,
|
||||||
@Email String email,
|
@Email String email,
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package de.tavolio.session;
|
|
||||||
|
|
||||||
import de.tavolio.session.dto.SessionCreation;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.ws.rs.POST;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
|
|
||||||
@Path("/sessions")
|
|
||||||
public class SessionResource
|
|
||||||
{
|
|
||||||
@Inject
|
|
||||||
Logger LOG;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SessionService sessionService;
|
|
||||||
|
|
||||||
@POST
|
|
||||||
public String get(@Valid SessionCreation sessionCreation)
|
|
||||||
{
|
|
||||||
String token = sessionService.generateBySessionCreation(sessionCreation);
|
|
||||||
LOG.infof("Generated token for email %s", sessionCreation.email());
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package de.tavolio.session;
|
|
||||||
|
|
||||||
import de.tavolio.account.AccountEntity;
|
|
||||||
import de.tavolio.account.AccountRepo;
|
|
||||||
import de.tavolio.session.dto.SessionCreation;
|
|
||||||
import io.quarkus.elytron.security.common.BcryptUtil;
|
|
||||||
import io.smallrye.jwt.build.Jwt;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.ws.rs.NotFoundException;
|
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class SessionService
|
|
||||||
{
|
|
||||||
@Inject
|
|
||||||
AccountRepo accountRepo;
|
|
||||||
|
|
||||||
public String generateBySessionCreation(SessionCreation sessionCreation)
|
|
||||||
{
|
|
||||||
Optional<AccountEntity> accountEntityOptional = accountRepo.findOptionalByEmail(sessionCreation.email());
|
|
||||||
if (accountEntityOptional.isPresent())
|
|
||||||
{
|
|
||||||
AccountEntity accountEntity = accountEntityOptional.get();
|
|
||||||
if (BcryptUtil.matches(sessionCreation.password(), accountEntity.getPassword()))
|
|
||||||
{
|
|
||||||
return generateToken(accountEntity.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateToken(String upn)
|
|
||||||
{
|
|
||||||
return Jwt.upn(upn).expiresAt(ZonedDateTime.now().plusYears(1).toInstant()).issuer("https://tavolio.de").sign();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
src/main/java/de/tavolio/superuser/SuperuserEntity.java
Normal file
37
src/main/java/de/tavolio/superuser/SuperuserEntity.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package de.tavolio.superuser;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "user_super")
|
||||||
|
public class SuperuserEntity
|
||||||
|
{
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public String getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SuperuserEntity setId(String id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword()
|
||||||
|
{
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SuperuserEntity setPassword(String password)
|
||||||
|
{
|
||||||
|
this.password = password;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/java/de/tavolio/superuser/SuperuserRepo.java
Normal file
9
src/main/java/de/tavolio/superuser/SuperuserRepo.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package de.tavolio.superuser;
|
||||||
|
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SuperuserRepo implements PanacheRepositoryBase<SuperuserEntity, String>
|
||||||
|
{
|
||||||
|
}
|
||||||
65
src/main/java/de/tavolio/verify/JwksService.java
Normal file
65
src/main/java/de/tavolio/verify/JwksService.java
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package de.tavolio.verify;
|
||||||
|
|
||||||
|
import de.tavolio.realm.key.KeypairEntity;
|
||||||
|
import de.tavolio.realm.key.KeypairRepo;
|
||||||
|
import de.tavolio.realm.RealmEntity;
|
||||||
|
import de.tavolio.verify.jwks.EcPublicKey;
|
||||||
|
import de.tavolio.verify.jwks.JwksKey;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class JwksService
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
KeypairRepo keypairRepo;
|
||||||
|
|
||||||
|
public Optional<JwksKey> findByKid(String kid)
|
||||||
|
{
|
||||||
|
KeypairEntity keypair = keypairRepo.findById(kid);
|
||||||
|
if (keypair != null)
|
||||||
|
{
|
||||||
|
switch (keypair.getType())
|
||||||
|
{
|
||||||
|
case "EC" ->
|
||||||
|
{
|
||||||
|
return Optional.of(constructPublicKey(keypair));
|
||||||
|
}
|
||||||
|
case "RSA" ->
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<JwksKey> findByRealm(RealmEntity realm)
|
||||||
|
{
|
||||||
|
List<JwksKey> result = new LinkedList<>();
|
||||||
|
for (KeypairEntity keypair : realm.getKeys())
|
||||||
|
{
|
||||||
|
switch (keypair.getType())
|
||||||
|
{
|
||||||
|
case "EC" ->
|
||||||
|
{
|
||||||
|
result.add(constructPublicKey(keypair));
|
||||||
|
}
|
||||||
|
case "RSA" ->
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EcPublicKey constructPublicKey(KeypairEntity entity)
|
||||||
|
{
|
||||||
|
return new EcPublicKey(entity.getType(), entity.getId(), entity.getUse(), entity.getUse(), entity.getCrv(), entity.getX(), entity.getY());
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/main/java/de/tavolio/verify/TokenVerifier.java
Normal file
81
src/main/java/de/tavolio/verify/TokenVerifier.java
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package de.tavolio.verify;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import de.tavolio.verify.jwks.JwksKey;
|
||||||
|
import io.quarkus.security.AuthenticationFailedException;
|
||||||
|
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||||
|
import io.quarkus.security.identity.IdentityProvider;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
|
||||||
|
import io.quarkus.security.runtime.QuarkusPrincipal;
|
||||||
|
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import io.smallrye.mutiny.infrastructure.Infrastructure;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.context.control.ActivateRequestContext;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class TokenVerifier implements IdentityProvider<TokenAuthenticationRequest>
|
||||||
|
{
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JwksService jwksService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<TokenAuthenticationRequest> getRequestType()
|
||||||
|
{
|
||||||
|
return TokenAuthenticationRequest.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@ActivateRequestContext
|
||||||
|
public Uni<SecurityIdentity> authenticate(TokenAuthenticationRequest tokenAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext)
|
||||||
|
{
|
||||||
|
String raw = tokenAuthenticationRequest.getToken().getToken();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Object kid = getHeader(raw).get("kid");
|
||||||
|
if (kid instanceof String keyId)
|
||||||
|
{
|
||||||
|
return Uni.createFrom().item(() -> {
|
||||||
|
Optional<JwksKey> keypair = jwksService.findByKid(keyId);
|
||||||
|
if (keypair.isPresent())
|
||||||
|
{
|
||||||
|
PublicKey publicKey = keypair.get().toPublicKey();
|
||||||
|
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal("")).build();
|
||||||
|
}
|
||||||
|
throw new AuthenticationFailedException();
|
||||||
|
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool());
|
||||||
|
}
|
||||||
|
return Uni.createFrom().nullItem();
|
||||||
|
}
|
||||||
|
catch (JsonProcessingException e)
|
||||||
|
{
|
||||||
|
return Uni.createFrom().failure(new AuthenticationFailedException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> getHeader(String raw) throws JsonProcessingException
|
||||||
|
{
|
||||||
|
return OBJECT_MAPPER.readValue(new String(Base64.getDecoder().decode(section(raw))), new TypeReference<Map<String, Object>>(){});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String section(String raw)
|
||||||
|
{
|
||||||
|
String[] sections = raw.split("\\.");
|
||||||
|
if (sections.length == 3)
|
||||||
|
{
|
||||||
|
return sections[0];
|
||||||
|
}
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/main/java/de/tavolio/verify/jwks/EcPublicKey.java
Normal file
64
src/main/java/de/tavolio/verify/jwks/EcPublicKey.java
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package de.tavolio.verify.jwks;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.AlgorithmParameters;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.*;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class EcPublicKey extends JwksKey
|
||||||
|
{
|
||||||
|
private final String crv;
|
||||||
|
private final String x;
|
||||||
|
private final String y;
|
||||||
|
|
||||||
|
public EcPublicKey(String kty, String kid, String use, String alg, String crv, String x, String y)
|
||||||
|
{
|
||||||
|
super(kty, kid, use, alg);
|
||||||
|
this.crv = crv;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCrv()
|
||||||
|
{
|
||||||
|
return crv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getX()
|
||||||
|
{
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getY()
|
||||||
|
{
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKey toPublicKey()
|
||||||
|
{
|
||||||
|
byte[] xBytes = Base64.getDecoder().decode(this.x);
|
||||||
|
byte[] yBytes = Base64.getDecoder().decode(this.y);
|
||||||
|
|
||||||
|
BigInteger x = new BigInteger(1, xBytes);
|
||||||
|
BigInteger y = new BigInteger(1, yBytes);
|
||||||
|
|
||||||
|
ECPoint point = new ECPoint(x, y);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AlgorithmParameters params = AlgorithmParameters.getInstance("EC");
|
||||||
|
params.init(new ECGenParameterSpec("secp256r1"));
|
||||||
|
ECParameterSpec spec = params.getParameterSpec(ECParameterSpec.class);
|
||||||
|
|
||||||
|
ECPublicKeySpec keySpec = new ECPublicKeySpec(point, spec);
|
||||||
|
return KeyFactory.getInstance("EC").generatePublic(keySpec);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/main/java/de/tavolio/verify/jwks/JwksKey.java
Normal file
41
src/main/java/de/tavolio/verify/jwks/JwksKey.java
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package de.tavolio.verify.jwks;
|
||||||
|
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
public abstract class JwksKey
|
||||||
|
{
|
||||||
|
private final String kty;
|
||||||
|
private final String kid;
|
||||||
|
private final String use;
|
||||||
|
private final String alg;
|
||||||
|
|
||||||
|
public JwksKey(String kty, String kid, String use, String alg)
|
||||||
|
{
|
||||||
|
this.kty = kty;
|
||||||
|
this.kid = kid;
|
||||||
|
this.use = use;
|
||||||
|
this.alg = alg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKty()
|
||||||
|
{
|
||||||
|
return kty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKid()
|
||||||
|
{
|
||||||
|
return kid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUse()
|
||||||
|
{
|
||||||
|
return use;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlg()
|
||||||
|
{
|
||||||
|
return alg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract PublicKey toPublicKey();
|
||||||
|
}
|
||||||
5
src/main/java/de/tavolio/verify/jwks/RsaPublicKey.java
Normal file
5
src/main/java/de/tavolio/verify/jwks/RsaPublicKey.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.tavolio.verify.jwks;
|
||||||
|
|
||||||
|
public class RsaPublicKey
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -3,33 +3,50 @@ quarkus.http.port=8089
|
|||||||
quarkus.http.test-port=9089
|
quarkus.http.test-port=9089
|
||||||
%dev.quarkus.http.host=0.0.0.0
|
%dev.quarkus.http.host=0.0.0.0
|
||||||
|
|
||||||
|
quarkus.http.cors.enabled=true
|
||||||
|
%dev.quarkus.http.cors.origins=/.*/
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
%prod.smallrye.jwt.sign.key.location=${PRIVATE_KEY_LOCATION}
|
%prod.smallrye.jwt.sign.key.location=${PRIVATE_KEY_LOCATION}
|
||||||
%prod.mp.jwt.verify.publickey.location=${PUBLIC_KEY_LOCATION}
|
%prod.mp.jwt.verify.publickey.location=${PUBLIC_KEY_LOCATION}
|
||||||
|
|
||||||
%dev.smallrye.jwt.sign.key.location=private.key
|
|
||||||
%dev.mp.jwt.verify.publickey.location=public.crt
|
|
||||||
mp.jwt.verify.issuer=https://tavolio.de
|
mp.jwt.verify.issuer=https://tavolio.de
|
||||||
|
|
||||||
|
%dev.smallrye.jwt.verify.key.location=/home/andreas/Documents/dev/publicKey.pem
|
||||||
|
%dev.smallrye.jwt.sign.key.location=/home/andreas/Documents/dev/privateKey.pem
|
||||||
|
|
||||||
# Postgres
|
# Postgres
|
||||||
prod.quarkus.hibernate-orm.validate-in-dev-mode=false
|
prod.quarkus.hibernate-orm.validate-in-dev-mode=false
|
||||||
quarkus.hibernate-orm.schema-management.strategy=none
|
quarkus.hibernate-orm.schema-management.strategy=none
|
||||||
|
%test,dev.quarkus.hibernate-orm.schema-management.strategy=drop-and-create
|
||||||
quarkus.datasource.db-kind=postgresql
|
quarkus.datasource.db-kind=postgresql
|
||||||
|
|
||||||
%dev.quarkus.datasource.username=postgres
|
%dev,test.quarkus.datasource.username=postgres
|
||||||
%dev.quarkus.datasource.password=${DB_PASSWORD}
|
%dev,test.quarkus.datasource.password=postgres
|
||||||
%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=iam
|
%dev,test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=auth
|
||||||
|
|
||||||
%prod.quarkus.datasource.username=${DB_USER}
|
%prod.quarkus.datasource.username=${DB_USER}
|
||||||
%prod.quarkus.datasource.password=${DB_PASSWORD}
|
%prod.quarkus.datasource.password=${DB_PASSWORD}
|
||||||
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?currentSchema=${DB_SCHEMA}
|
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?currentSchema=${DB_SCHEMA}
|
||||||
|
|
||||||
# Flyway
|
# Flyway
|
||||||
|
quarkus.flyway.enabled=false
|
||||||
%test.quarkus.flyway.clean-at-start=true
|
%test.quarkus.flyway.clean-at-start=true
|
||||||
%dev.quarkus.flyway.clean-at-start=true
|
%dev.quarkus.flyway.clean-at-start=true
|
||||||
%dev.quarkus.flyway.locations=db/migration,db/dev
|
%dev.quarkus.flyway.locations=db/migration,db/dev
|
||||||
|
%test,dev.quarkus.flyway.migrate-at-start=false
|
||||||
quarkus.flyway.migrate-at-start=true
|
quarkus.flyway.migrate-at-start=true
|
||||||
|
|
||||||
# IAM Superuser
|
# IAM Superuser
|
||||||
%test,dev.iam.user.name=tavolio
|
%test,dev.iam.user.name=tavolio
|
||||||
%test,dev.iam.user.password=tavolio
|
%test,dev.iam.user.password=tavolio
|
||||||
|
|
||||||
|
|
||||||
|
quarkus.http.access-log.enabled=true
|
||||||
|
quarkus.http.auth.basic=true
|
||||||
|
dev.dinauer.idp.origin=http://localhost:8089
|
||||||
|
|
||||||
|
%dev.dev.dinauer.idp.superuser.username=admin
|
||||||
|
%dev.dev.dinauer.idp.superuser.password=pw
|
||||||
|
|
||||||
|
quarkus.log.level=DEBUG
|
||||||
22
src/main/resources/bootstrap.yaml
Normal file
22
src/main/resources/bootstrap.yaml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
realms:
|
||||||
|
maven:
|
||||||
|
name: My Bootstrap Realm
|
||||||
|
audience:
|
||||||
|
strategy: realm
|
||||||
|
key:
|
||||||
|
type: EC
|
||||||
|
alg: P256
|
||||||
|
clients:
|
||||||
|
backend:
|
||||||
|
client-secret: MY_SECRET_ENV
|
||||||
|
permissions:
|
||||||
|
- USER:VIEW
|
||||||
|
frontend:
|
||||||
|
redirect-uri: http://localhost:8080/callback
|
||||||
|
accounts:
|
||||||
|
- email: andreas.j.dinauer@gmail.com
|
||||||
|
first-name: Andreas
|
||||||
|
last-name: Dinauer
|
||||||
|
password-plain: pw
|
||||||
|
roles:
|
||||||
|
- USER:VIEW
|
||||||
@ -1,5 +1,2 @@
|
|||||||
INSERT INTO account (id, firstname, lastname, email, account_password, status)
|
INSERT INTO account (id, firstname, lastname, email, account_password, status)
|
||||||
VALUES ('66b261fe-4c5a-4728-9857-67717f02d4e1', 'Andreas', 'Dinauer', 'andreas.j.dinauer@gmail.com', '$2a$12$cdrzIY4sMFAXiz29uo9Ul.MPy0RN0FGS2yjVzb5BTe6bSijn4eGQy', 'REGISTERED');
|
VALUES ('66b261fe-4c5a-4728-9857-67717f02d4e1', 'Andreas', 'Dinauer', 'andreas.j.dinauer@gmail.com', '$2a$12$cdrzIY4sMFAXiz29uo9Ul.MPy0RN0FGS2yjVzb5BTe6bSijn4eGQy', 'REGISTERED');
|
||||||
|
|
||||||
INSERT INTO membership(id, tenant_type, tenant_id, member_role, member_since, account_id)
|
|
||||||
VALUES ('cd20d271-6e76-48c4-9cfb-a67f8892d46c', 'ORGANISATION', 'b3912be6-7503-4a13-b8ab-5d65af036742', 'OWNER', '2025-08-20 15:30:12.123456+02', '66b261fe-4c5a-4728-9857-67717f02d4e1');
|
|
||||||
@ -8,16 +8,3 @@ CREATE TABLE account (
|
|||||||
CONSTRAINT account_pkey PRIMARY KEY (id),
|
CONSTRAINT account_pkey PRIMARY KEY (id),
|
||||||
CONSTRAINT account_status_check CHECK (((status)::text = ANY ((ARRAY['INIT'::character varying, 'REGISTERED'::character varying])::text[])))
|
CONSTRAINT account_status_check CHECK (((status)::text = ANY ((ARRAY['INIT'::character varying, 'REGISTERED'::character varying])::text[])))
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE membership (
|
|
||||||
member_since TIMESTAMP(6) WITH TIME ZONE NOT NULL,
|
|
||||||
account_id VARCHAR(255) NOT NULL,
|
|
||||||
id VARCHAR(255) NOT NULL,
|
|
||||||
member_role VARCHAR(255) NOT NULL,
|
|
||||||
tenant_id VARCHAR(255) NOT NULL,
|
|
||||||
tenant_type VARCHAR(255) NOT NULL,
|
|
||||||
CONSTRAINT membership_member_role_check CHECK (((member_role)::text = ANY ((ARRAY['OWNER'::character varying, 'ADMIN'::character varying, 'MEMBER'::character varying])::text[]))),
|
|
||||||
CONSTRAINT membership_pkey PRIMARY KEY (id),
|
|
||||||
CONSTRAINT membership_tenant_type_check CHECK (((tenant_type)::text = ANY ((ARRAY['ORGANISATION'::character varying, 'RESTAURANT'::character varying])::text[]))),
|
|
||||||
CONSTRAINT fkd1yliqdvipm4yvq2tulbktpg6 FOREIGN KEY (account_id) REFERENCES account(id)
|
|
||||||
);
|
|
||||||
0
src/main/resources/import.sql
Normal file
0
src/main/resources/import.sql
Normal file
@ -1,5 +1,7 @@
|
|||||||
package de.tavolio.account;
|
package de.tavolio.account;
|
||||||
|
|
||||||
|
import de.tavolio.realm.user.UserEntity;
|
||||||
|
import de.tavolio.realm.user.UserRepo;
|
||||||
import de.tavolio.utils.Database;
|
import de.tavolio.utils.Database;
|
||||||
import io.quarkus.elytron.security.common.BcryptUtil;
|
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
@ -15,13 +17,13 @@ import static io.restassured.RestAssured.when;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@QuarkusTest
|
@QuarkusTest
|
||||||
public class AccountResourceTest
|
public class UserResourceTest
|
||||||
{
|
{
|
||||||
@Inject
|
@Inject
|
||||||
Database database;
|
Database database;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AccountRepo accountRepo;
|
UserRepo userRepo;
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void afterEach()
|
void afterEach()
|
||||||
@ -49,7 +51,7 @@ public class AccountResourceTest
|
|||||||
.then()
|
.then()
|
||||||
.statusCode(200);
|
.statusCode(200);
|
||||||
|
|
||||||
assertEquals(1, accountRepo.count());
|
assertEquals(1, userRepo.count());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -58,13 +60,13 @@ public class AccountResourceTest
|
|||||||
{
|
{
|
||||||
// given
|
// given
|
||||||
database.setup(() -> {
|
database.setup(() -> {
|
||||||
AccountEntity account = new AccountEntity()
|
UserEntity account = new UserEntity()
|
||||||
.setId("66609092-6c98-4466-af52-9a3e9d633108")
|
.setId("66609092-6c98-4466-af52-9a3e9d633108")
|
||||||
.setFirstname("Andreas")
|
.setFirstname("Andreas")
|
||||||
.setLastname("Dinauer")
|
.setLastname("Dinauer")
|
||||||
.setEmail("andreas.j.dinauer@gmail.com")
|
.setEmail("andreas.j.dinauer@gmail.com")
|
||||||
.setPassword(BcryptUtil.bcryptHash("pw"));
|
.setPassword(BcryptUtil.bcryptHash("pw"));
|
||||||
accountRepo.persist(account);
|
userRepo.persist(account);
|
||||||
});
|
});
|
||||||
|
|
||||||
// when
|
// when
|
||||||
19
src/test/java/de/tavolio/realm/RealmServiceTest.java
Normal file
19
src/test/java/de/tavolio/realm/RealmServiceTest.java
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package de.tavolio.realm;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
public class RealmServiceTest
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
RealmService realmService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test()
|
||||||
|
{
|
||||||
|
realmService.create(new RealmCreation("Test Realm", "test-realm"));
|
||||||
|
System.out.println("Press any key to continue...");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user