♻️ Transform to IDP provider

This commit is contained in:
Andreas Dinauer 2026-03-08 08:59:03 +01:00
parent 7cdf27f372
commit 79104dd02f
91 changed files with 2572 additions and 791 deletions

13
pom.xml
View File

@ -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
View 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"

View File

@ -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());
}
} }

View File

@ -0,0 +1,6 @@
package de.tavolio;
public enum Role
{
ROOT, CLIENT, USER
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -1,6 +0,0 @@
package de.tavolio.account;
public enum AccountStatus
{
INIT, REGISTERED
}

View File

@ -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)
{
}

View 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;
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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());
}
}
}

View File

@ -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);
}
}
}

View 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));
}
}

View File

@ -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);
}
}
}

View 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)
{
}

View File

@ -0,0 +1,5 @@
package de.tavolio.bootstrap.model;
public record Audience(String strategy, String value)
{
}

View File

@ -0,0 +1,7 @@
package de.tavolio.bootstrap.model;
import java.util.Map;
public record Bootstrap(Map<String, Realm> realms)
{
}

View 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)
{
}

View File

@ -0,0 +1,5 @@
package de.tavolio.bootstrap.model;
public record Key(String type, String alg)
{
}

View 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)
{
}

View File

@ -0,0 +1,7 @@
package de.tavolio.bootstrap.model;
import java.util.List;
public record Role(List<String> permissions)
{
}

View File

@ -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))
);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -1,6 +0,0 @@
package de.tavolio.member;
public enum MembershipRole
{
OWNER, ADMIN, MEMBER
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -1,6 +0,0 @@
package de.tavolio.member;
public enum TenantType
{
ORGANISATION, RESTAURANT
}

View File

@ -1,7 +0,0 @@
package de.tavolio.member.dto;
import java.util.List;
public record AccountMemberships(List<Membership> organisations, List<Membership> restaurants)
{
}

View File

@ -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)
{
}

View File

@ -1,7 +0,0 @@
package de.tavolio.member.dto;
import de.tavolio.member.MembershipRole;
public record MembershipCreation(String accountId, MembershipRole role)
{
}

View 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);
}
}

View 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);
}
}

View 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();
}
}

View 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;
}
}

View 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();
}
}

View 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();
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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;

View 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);
}
}

View 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();
}
}

View 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;
}
}

View File

@ -0,0 +1,5 @@
package de.tavolio.realm;
public record RealmCreation(String name, String key)
{
}

View 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;
}
}

View 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();
}
}

View 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();
}
}

View File

@ -0,0 +1,6 @@
package de.tavolio.realm;
public interface RealmScoped
{
RealmEntity getRealm();
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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>
{
}

View File

@ -0,0 +1,5 @@
package de.tavolio.realm.client;
public record ClientCreation(String id, String secret)
{
}

View 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;
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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>
{
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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());
}
}

View 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));
}
}

View 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);
}
}

View 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;
}
}

View File

@ -0,0 +1,6 @@
package de.tavolio.realm.user;
public enum UserStatus
{
INIT, REGISTERED
}

View 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)
{
}

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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();
}
}

View 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;
}
}

View 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>
{
}

View 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());
}
}

View 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();
}
}

View 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);
}
}
}

View 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();
}

View File

@ -0,0 +1,5 @@
package de.tavolio.verify.jwks;
public class RsaPublicKey
{
}

View File

@ -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

View 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

View File

@ -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');

View File

@ -7,17 +7,4 @@ CREATE TABLE account (
status VARCHAR(255) NULL, status VARCHAR(255) NULL,
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)
); );

View File

View 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

View 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...");
}
}