🚧 Improvements

This commit is contained in:
Andreas Dinauer 2026-04-06 12:09:13 +02:00
parent 38bed80c5a
commit 6444931d73
15 changed files with 249 additions and 61 deletions

View File

@ -34,10 +34,10 @@ public class ClientBootstrapper
String name = bootstrap.getKey();
Client client = bootstrap.getValue();
ClientEntity entity = new ClientEntity()
.setId(UUID.randomUUID().toString().replace("-", "").substring(0, 16))
.setId(UUID.randomUUID().toString())
.setName(name)
.setSecret(Credentials.resolve(client.secret()))
.setRedirectURI(client.redirectURI())
.setAllowedRedirectURIs(client.redirectURI())
.setRealm(realm)
.setPermissions(client.permissions())
.setAllowedGrants(client.allowedGrants());

View File

@ -4,10 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import de.tavolio.realm.client.Grant;
import de.tavolio.realm.user.Permission;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uri") String redirectURI, @JsonProperty("permissions") Set<String> permissionList, @JsonProperty("allowed-grants") Set<Grant> allowedGrants)
public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uris") Set<String> redirectURI, @JsonProperty("permissions") Set<String> permissionList, @JsonProperty("allowed-grants") Set<Grant> allowedGrants)
{
public Set<Permission> permissions()
{

View File

@ -17,6 +17,8 @@ import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
@ -26,6 +28,8 @@ import java.util.Map;
@RequestScoped
public class OidcResource
{
private static final Logger LOG = LoggerFactory.getLogger(OidcResource.class);
@PathParam("realm-key")
String realmKey;
@ -59,17 +63,21 @@ public class OidcResource
@POST
@Path("/auth")
public Response auth(@QueryParam("client_id") String clientId, @FormParam("email") String email, @FormParam("password") String password)
public Response auth(@QueryParam("client_id") String clientId, @QueryParam("redirect_uri") String redirectURI, @FormParam("email") String email, @FormParam("password") String password)
{
RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByNameAndRealm(clientId, realm);
if (client != null)
if (client.getAllowedRedirectURIs().contains(redirectURI))
{
String code = authorizationService.generateBySessionCreation(realmKey, clientId, new AuthorizationCreation(email, password));
return Response.status(302).location(URI.create(client.getRedirectURI() + "?code=" + code + "&state=d")).build();
return Response.status(302).location(URI.create(redirectURI + "?code=" + code)).build();
}
else
{
LOG.error("Invalid redirect uri provided: {}", redirectURI);
throw new BadRequestException();
}
}
@Authenticated
@POST
@ -83,6 +91,7 @@ public class OidcResource
ClientEntity client = clientService.requireByNameAndRealm(identity.getPrincipal().getName(), realm);
if (!client.getAllowedGrants().contains(grant))
{
LOG.error("Client {} has no permission for grant {}", client.getName(), grant);
throw new ForbiddenException();
}
@ -107,6 +116,7 @@ public class OidcResource
{
return Grant.CLIENT_CREDENTIALS;
}
LOG.error("Invalid grant {} provided", grantType);
throw new RuntimeException();
}
}

View File

@ -37,7 +37,6 @@ public class AuthorizationService
@Transactional
public String generateBySessionCreation(String realmKey, String clientId, AuthorizationCreation authorizationCreation)
{
RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByNameAndRealm(clientId, realm);
Optional<UserEntity> accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, authorizationCreation.email());

View File

@ -26,6 +26,17 @@ public class UserTokenGenerator
.sign(key);
}
public String generateRefreshToken(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);
}
public String generateIDToken(String realmKey, String clientId, String upn, ZonedDateTime expiresAt, PrivateKey key, String keyId)
{
return Jwt.claims()

View File

@ -13,6 +13,8 @@ import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
@ -25,6 +27,8 @@ import java.util.UUID;
@ApplicationScoped
public class UserTokenService
{
private static final Logger LOG = LoggerFactory.getLogger(UserTokenService.class);
@Inject
CodeRepo codeRepo;
@ -37,30 +41,36 @@ public class UserTokenService
@Inject
RealmService realmService;
public TokenResponse getToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException
public TokenResponse getToken(String realmKey, String code)
{
return generateUserToken(realmKey, code);
}
private TokenResponse generateUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException
private TokenResponse generateUserToken(String realmKey, String code)
{
String principal = identity.getPrincipal().getName();
RealmEntity realm = realmService.requireByKey(realmKey);
CodeEntity entity = codeRepo.findByRealmAndId(realm, code);
if (entity != null && !ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getName()))
if (entity != null)
{
if (!ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getName()))
{
KeypairEntity keypair = realm.getKeys().getFirst();
PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair);
ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(realm.getLifetime());
ZonedDateTime refreshTokenExpiresAt = ZonedDateTime.now().plusDays(3);
TokenResponse response = new TokenResponse()
.setAccessToken(userTokenGenerator.generateAccessToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId()))
.setRefreshToken(UUID.randomUUID().toString())
.setRefreshToken(userTokenGenerator.generateRefreshToken(realm.getKey(), principal, entity.getAccount().getId(), refreshTokenExpiresAt, signingKey, keypair.getId()))
.setIdToken(userTokenGenerator.generateIDToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId()))
.setTokenType("Bearer")
.setExpiresAt(expiresAt.toInstant().getEpochSecond());
codeRepo.delete(entity);
return response;
}
LOG.error("Code is expired or requesting client is not allowed to exchange code.");
throw new BadRequestException();
}
throw new BadRequestException();
}
}

View File

@ -2,6 +2,6 @@ package de.tavolio.realm.client;
import java.util.Set;
public record Client(String id, String name, String redirectURI, Set<Grant> allowedGrants)
public record Client(String id, String name, Set<String> redirectURI, Set<Grant> allowedGrants)
{
}

View File

@ -2,6 +2,6 @@ package de.tavolio.realm.client;
import java.util.Set;
public record ClientCreation(String name, String secret, String redirectURI, Set<Grant> allowedGrants)
public record ClientCreation(String name, String secret, Set<String> redirectURI, Set<Grant> allowedGrants)
{
}

View File

@ -23,9 +23,6 @@ public class ClientEntity implements RealmScoped
private String secret;
@Column(name = "redirect_uri")
private String redirectURI;
@ManyToOne
@JoinColumn(name = "realm_id")
private RealmEntity realm;
@ -45,6 +42,11 @@ public class ClientEntity implements RealmScoped
@Enumerated(EnumType.STRING)
private Set<Grant> allowedGrants = new HashSet<>();
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "allowed_redirect_uri", joinColumns = @JoinColumn(name = "client_id"))
@Column(name = "redirect_uri")
private Set<String> allowedRedirectURIs = new HashSet<>();
public String getId()
{
return id;
@ -89,17 +91,6 @@ public class ClientEntity implements RealmScoped
return this;
}
public String getRedirectURI()
{
return redirectURI;
}
public ClientEntity setRedirectURI(String redirectURI)
{
this.redirectURI = redirectURI;
return this;
}
public List<CodeEntity> getCodes()
{
return codes;
@ -132,4 +123,15 @@ public class ClientEntity implements RealmScoped
this.allowedGrants = allowedGrants;
return this;
}
public Set<String> getAllowedRedirectURIs()
{
return allowedRedirectURIs;
}
public ClientEntity setAllowedRedirectURIs(Set<String> allowedRedirectURIs)
{
this.allowedRedirectURIs = allowedRedirectURIs;
return this;
}
}

View File

@ -11,6 +11,6 @@ public class ClientMapper
public static Client map(ClientEntity client)
{
return new Client(client.getId(), client.getName(), client.getRedirectURI(), client.getAllowedGrants());
return new Client(client.getId(), client.getName(), client.getAllowedRedirectURIs(), client.getAllowedGrants());
}
}

View File

@ -35,14 +35,14 @@ public class ClientService
{
RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity entity = new ClientEntity();
entity.setId(UUID.randomUUID().toString().replace("-", "").substring(0, 16));
entity.setId(UUID.randomUUID().toString());
entity.setName(client.name());
if (!StringUtils.isBlank(client.secret()))
{
entity.setSecret(BcryptUtil.bcryptHash(client.secret()));
}
entity.setAllowedGrants(client.allowedGrants());
entity.setRedirectURI(client.redirectURI());
entity.setAllowedRedirectURIs(client.redirectURI());
entity.setRealm(realm);
clientRepo.persist(entity);
return ClientMapper.map(entity);
@ -67,7 +67,7 @@ public class ClientService
if (existing != null)
{
existing.setName(client.name());
existing.setRedirectURI(client.redirectURI());
existing.setAllowedRedirectURIs(client.redirectURI());
existing.setAllowedGrants(client.allowedGrants());
clientRepo.persist(existing);
return ClientMapper.map(existing);

View File

@ -9,7 +9,7 @@ quarkus.http.cors.enabled=true
# Postgres
prod.quarkus.hibernate-orm.validate-in-dev-mode=false
quarkus.hibernate-orm.schema-management.strategy=none
%test,dev.quarkus.hibernate-orm.schema-management.strategy=drop-and-create
%test,dev.quarkus.hibernate-orm.schema-management.strategy=none
quarkus.datasource.db-kind=postgresql
%dev,test.quarkus.datasource.username=postgres
@ -17,14 +17,11 @@ quarkus.datasource.db-kind=postgresql
%dev,test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=auth
# Flyway
quarkus.flyway.enabled=false
%test.quarkus.flyway.clean-at-start=true
%dev.quarkus.flyway.clean-at-start=true
quarkus.flyway.enabled=true
quarkus.flyway.clean-at-start=true
%dev.quarkus.flyway.locations=db/migration,db/dev
%test,dev.quarkus.flyway.migrate-at-start=false
quarkus.flyway.migrate-at-start=true
quarkus.http.access-log.enabled=true
quarkus.http.auth.basic=false
%dev.io.verifoo.http.origin=http://localhost:8089

View File

@ -15,11 +15,13 @@ realms:
backend:
secret:
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
redirect-uri: http://localhost:8080/auth/callback
redirect-uris:
- http://localhost:8080/auth/callback
permissions:
- USER_VIEW
allowed-grants:
- AUTHORIZATION_CODE
- CLIENT_CREDENTIALS
analytics-backend:
secret:
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.

View File

@ -1,2 +0,0 @@
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');

View File

@ -1,10 +1,168 @@
CREATE TABLE account (
email VARCHAR(255) NULL,
firstname VARCHAR(255) NULL,
id VARCHAR(255) NOT NULL,
lastname VARCHAR(255) NULL,
account_password VARCHAR(255) NULL,
status VARCHAR(255) NULL,
CONSTRAINT account_pkey PRIMARY KEY (id),
CONSTRAINT account_status_check CHECK (((status)::text = ANY ((ARRAY['INIT'::character varying, 'REGISTERED'::character varying])::text[])))
create table realm
(
lifetime integer not null,
id varchar(255) not null primary key,
key varchar(255),
realm_name varchar(255)
);
alter table realm
owner to postgres;
create table audience_strategy
(
id varchar(255) not null primary key,
realm_id varchar(255) unique
constraint fkth1885jeywykkufsp36n5tc56 references realm,
strategy varchar(255),
value varchar(255)
);
alter table audience_strategy
owner to postgres;
create table client
(
id varchar(255) not null primary key,
name varchar(255),
realm_id varchar(255)
constraint fk1bi8c7s4u3s44vw3w8le6xau1 references realm on delete cascade,
secret varchar(255)
);
create table allowed_grants
(
client_id varchar(255) not null
constraint fkpuqnyqud9ft1tn18qrcy7457c references client,
grant_name varchar(255)
constraint allowed_grants_grant_name_check check ((grant_name)::text = ANY ((ARRAY ['CLIENT_CREDENTIALS':: character varying, 'AUTHORIZATION_CODE':: character varying, 'PASSWORD':: character varying])::text[])
)
);
alter table allowed_grants
owner to postgres;
create table allowed_redirect_uri
(
client_id varchar(255) not null
constraint fkamn8o2599ky2nactoc7umiltg references client,
redirect_uri varchar(255)
);
alter table allowed_redirect_uri
owner to postgres;
create table client_permission
(
client_id varchar(255) not null
constraint fk1kqfe7d8w5bor8vp0aoaloptt
references client,
permission varchar(255)
constraint client_permission_permission_check
check ((permission)::text = ANY
((ARRAY ['USER_VIEW':: character varying, 'USER_DELETE':: character varying, 'USER_CREATE':: character varying])::text[])
)
);
alter table client_permission
owner to postgres;
create table keypair
(
alg varchar(255),
crv varchar(255),
id varchar(255) not null
primary key,
realm_id varchar(255)
constraint fk1ceq9els4s8jqs96xnmi2y3bi
references realm
on delete cascade,
type varchar(255),
use varchar(255),
x varchar(255),
y varchar(255),
privatekey bytea
);
alter table keypair
owner to postgres;
create table role
(
id varchar(255) not null
primary key,
realm_id varchar(255)
constraint fkocsid53ns4d9sngf84ab78bsr
references realm
on delete cascade,
role_name varchar(255)
);
alter table role
owner to postgres;
create table user_regular
(
account_password varchar(255),
email varchar(255),
firstname varchar(255),
id varchar(255) not null
primary key,
lastname varchar(255),
realm_id varchar(255)
constraint fki4jjeaqdf1op17hosriwn55t2
references realm
on delete cascade,
status varchar(255)
constraint user_regular_status_check
check ((status)::text = ANY ((ARRAY ['INIT':: character varying, 'REGISTERED':: character varying])::text[])
)
);
alter table user_regular
owner to postgres;
create table assignment
(
id varchar(255) not null
primary key,
role_id varchar(255)
constraint fk90vsrx78n7k7tjr42g7xx6hmy
references role,
user_id varchar(255)
constraint fk3gqvh1h60fkikoc5ix1hvaovl
references user_regular
);
alter table assignment
owner to postgres;
create table code
(
expiresat timestamp(6) with time zone,
account_id varchar(255)
constraint fk7kkfrwxn8bry4boaorheikjcu
references user_regular,
client_id varchar(255)
constraint fkgs7a5cpyl2slw4j5ageba4hlv
references client,
id varchar(255) not null
primary key,
realm_id varchar(255)
constraint fkqp7vcsghxg1dag4t89ys7mmfq
references realm
on delete cascade
);
alter table code
owner to postgres;
create table user_super
(
id varchar(255) not null
primary key,
password varchar(255)
);
alter table user_super
owner to postgres;