🚧 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(); String name = bootstrap.getKey();
Client client = bootstrap.getValue(); Client client = bootstrap.getValue();
ClientEntity entity = new ClientEntity() ClientEntity entity = new ClientEntity()
.setId(UUID.randomUUID().toString().replace("-", "").substring(0, 16)) .setId(UUID.randomUUID().toString())
.setName(name) .setName(name)
.setSecret(Credentials.resolve(client.secret())) .setSecret(Credentials.resolve(client.secret()))
.setRedirectURI(client.redirectURI()) .setAllowedRedirectURIs(client.redirectURI())
.setRealm(realm) .setRealm(realm)
.setPermissions(client.permissions()) .setPermissions(client.permissions())
.setAllowedGrants(client.allowedGrants()); .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.client.Grant;
import de.tavolio.realm.user.Permission; import de.tavolio.realm.user.Permission;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; 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() public Set<Permission> permissions()
{ {

View File

@ -17,6 +17,8 @@ import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI; import java.net.URI;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -26,6 +28,8 @@ import java.util.Map;
@RequestScoped @RequestScoped
public class OidcResource public class OidcResource
{ {
private static final Logger LOG = LoggerFactory.getLogger(OidcResource.class);
@PathParam("realm-key") @PathParam("realm-key")
String realmKey; String realmKey;
@ -59,16 +63,20 @@ public class OidcResource
@POST @POST
@Path("/auth") @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); RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByNameAndRealm(clientId, realm); ClientEntity client = clientService.requireByNameAndRealm(clientId, realm);
if (client != null) if (client.getAllowedRedirectURIs().contains(redirectURI))
{ {
String code = authorizationService.generateBySessionCreation(realmKey, clientId, new AuthorizationCreation(email, password)); 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();
} }
throw new BadRequestException();
} }
@Authenticated @Authenticated
@ -83,6 +91,7 @@ public class OidcResource
ClientEntity client = clientService.requireByNameAndRealm(identity.getPrincipal().getName(), realm); ClientEntity client = clientService.requireByNameAndRealm(identity.getPrincipal().getName(), realm);
if (!client.getAllowedGrants().contains(grant)) if (!client.getAllowedGrants().contains(grant))
{ {
LOG.error("Client {} has no permission for grant {}", client.getName(), grant);
throw new ForbiddenException(); throw new ForbiddenException();
} }
@ -107,6 +116,7 @@ public class OidcResource
{ {
return Grant.CLIENT_CREDENTIALS; return Grant.CLIENT_CREDENTIALS;
} }
LOG.error("Invalid grant {} provided", grantType);
throw new RuntimeException(); throw new RuntimeException();
} }
} }

View File

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

View File

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

View File

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

View File

@ -2,6 +2,6 @@ package de.tavolio.realm.client;
import java.util.Set; 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; 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; private String secret;
@Column(name = "redirect_uri")
private String redirectURI;
@ManyToOne @ManyToOne
@JoinColumn(name = "realm_id") @JoinColumn(name = "realm_id")
private RealmEntity realm; private RealmEntity realm;
@ -45,6 +42,11 @@ public class ClientEntity implements RealmScoped
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private Set<Grant> allowedGrants = new HashSet<>(); 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() public String getId()
{ {
return id; return id;
@ -89,17 +91,6 @@ public class ClientEntity implements RealmScoped
return this; return this;
} }
public String getRedirectURI()
{
return redirectURI;
}
public ClientEntity setRedirectURI(String redirectURI)
{
this.redirectURI = redirectURI;
return this;
}
public List<CodeEntity> getCodes() public List<CodeEntity> getCodes()
{ {
return codes; return codes;
@ -132,4 +123,15 @@ public class ClientEntity implements RealmScoped
this.allowedGrants = allowedGrants; this.allowedGrants = allowedGrants;
return this; 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) 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); RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity entity = new ClientEntity(); ClientEntity entity = new ClientEntity();
entity.setId(UUID.randomUUID().toString().replace("-", "").substring(0, 16)); entity.setId(UUID.randomUUID().toString());
entity.setName(client.name()); entity.setName(client.name());
if (!StringUtils.isBlank(client.secret())) if (!StringUtils.isBlank(client.secret()))
{ {
entity.setSecret(BcryptUtil.bcryptHash(client.secret())); entity.setSecret(BcryptUtil.bcryptHash(client.secret()));
} }
entity.setAllowedGrants(client.allowedGrants()); entity.setAllowedGrants(client.allowedGrants());
entity.setRedirectURI(client.redirectURI()); entity.setAllowedRedirectURIs(client.redirectURI());
entity.setRealm(realm); entity.setRealm(realm);
clientRepo.persist(entity); clientRepo.persist(entity);
return ClientMapper.map(entity); return ClientMapper.map(entity);
@ -67,7 +67,7 @@ public class ClientService
if (existing != null) if (existing != null)
{ {
existing.setName(client.name()); existing.setName(client.name());
existing.setRedirectURI(client.redirectURI()); existing.setAllowedRedirectURIs(client.redirectURI());
existing.setAllowedGrants(client.allowedGrants()); existing.setAllowedGrants(client.allowedGrants());
clientRepo.persist(existing); clientRepo.persist(existing);
return ClientMapper.map(existing); return ClientMapper.map(existing);

View File

@ -9,7 +9,7 @@ quarkus.http.cors.enabled=true
# 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 %test,dev.quarkus.hibernate-orm.schema-management.strategy=none
quarkus.datasource.db-kind=postgresql quarkus.datasource.db-kind=postgresql
%dev,test.quarkus.datasource.username=postgres %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 %dev,test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=auth
# Flyway # Flyway
quarkus.flyway.enabled=false quarkus.flyway.enabled=true
%test.quarkus.flyway.clean-at-start=true 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
quarkus.http.access-log.enabled=true
quarkus.http.auth.basic=false quarkus.http.auth.basic=false
%dev.io.verifoo.http.origin=http://localhost:8089 %dev.io.verifoo.http.origin=http://localhost:8089

View File

@ -15,11 +15,13 @@ realms:
backend: backend:
secret: secret:
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX. bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
redirect-uri: http://localhost:8080/auth/callback redirect-uris:
- http://localhost:8080/auth/callback
permissions: permissions:
- USER_VIEW - USER_VIEW
allowed-grants: allowed-grants:
- AUTHORIZATION_CODE - AUTHORIZATION_CODE
- CLIENT_CREDENTIALS
analytics-backend: analytics-backend:
secret: secret:
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX. 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 ( create table realm
email VARCHAR(255) NULL, (
firstname VARCHAR(255) NULL, lifetime integer not null,
id VARCHAR(255) NOT NULL, id varchar(255) not null primary key,
lastname VARCHAR(255) NULL, key varchar(255),
account_password VARCHAR(255) NULL, realm_name varchar(255)
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[]))) 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;