🚧 Changes

This commit is contained in:
Andreas Dinauer 2026-04-04 19:13:40 +02:00
parent cf0b994eed
commit 483d34457f
43 changed files with 778 additions and 210 deletions

View File

@ -0,0 +1,79 @@
package de.tavolio.auth.httpmechanism;
import de.tavolio.auth.request.JWTRequest;
import de.tavolio.auth.request.UsernamePasswordRequest;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import org.apache.commons.lang3.Strings;
import java.util.Base64;
@Priority(50)
@ApplicationScoped
public class CustomHttpAuthenticationMechanism implements HttpAuthenticationMechanism
{
private static final String BASIC = "Basic ";
private static final String BEARER = "Bearer ";
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager)
{
String realmKey = getRealmKey(context.request().uri());
context.put(CustomHttpAuthenticationMechanism.class.getName(), this);
String auth = context.request().headers().get(HttpHeaders.AUTHORIZATION);
if (Strings.CI.startsWith(auth, BASIC))
{
String decoded = new String(Base64.getDecoder().decode(Strings.CI.removeStart(auth, BASIC)));
String[] sections = decoded.split(":");
if (sections.length == 2)
{
UsernamePasswordRequest request = new UsernamePasswordRequest(realmKey, sections[0], sections[1]);
HttpSecurityUtils.setRoutingContextAttribute(request, context);
return identityProviderManager.authenticate(request);
}
return Uni.createFrom().failure(new AuthenticationFailedException());
}
if (Strings.CI.startsWith(auth, BEARER))
{
JWTRequest request = new JWTRequest(realmKey, Strings.CI.removeStart(auth, BEARER));
HttpSecurityUtils.setRoutingContextAttribute(request, context);
return identityProviderManager.authenticate(request);
}
return Uni.createFrom().nullItem();
}
private String getRealmKey(String uri)
{
String[] sections = uri.split("/");
for (int i = 0; i < sections.length; i++)
{
if (sections[i].equals("realms"))
{
try
{
return sections[i + 1];
}
catch (Exception e)
{
return null;
}
}
}
return null;
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context)
{
return null;
}
}

View File

@ -1,7 +1,8 @@
package de.tavolio.oidc.identityproviders;
package de.tavolio.auth.identityproviders;
import de.tavolio.Role;
import de.tavolio.realm.user.UserRepo;
import de.tavolio.auth.request.UsernamePasswordRequest;
import de.tavolio.realm.RealmService;
import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.client.ClientRepo;
import de.tavolio.superuser.SuperuserEntity;
@ -11,7 +12,6 @@ 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;
@ -23,7 +23,7 @@ import jakarta.inject.Inject;
@ApplicationScoped
@Priority(1)
public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest>
public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePasswordRequest>
{
@Inject
ClientRepo clientRepo;
@ -31,19 +31,22 @@ public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePassw
@Inject
SuperuserRepo superuserRepo;
@Inject
RealmService realmService;
@Override
public Class<UsernamePasswordAuthenticationRequest> getRequestType()
public Class<UsernamePasswordRequest> getRequestType()
{
return UsernamePasswordAuthenticationRequest.class;
return UsernamePasswordRequest.class;
}
@Override
@ActivateRequestContext
public Uni<SecurityIdentity> authenticate(UsernamePasswordAuthenticationRequest usernamePasswordAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext)
public Uni<SecurityIdentity> authenticate(UsernamePasswordRequest usernamePasswordAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext)
{
return Uni.createFrom().item(() -> {
String username = usernamePasswordAuthenticationRequest.getUsername();
String credential = new String(usernamePasswordAuthenticationRequest.getPassword().getPassword());
String credential = usernamePasswordAuthenticationRequest.getPassword();
SuperuserEntity superuser = superuserRepo.findById(username);
if (superuser != null)
{
@ -52,7 +55,7 @@ public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePassw
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(username)).addRole(Role.ROOT.toString()).build();
}
}
ClientEntity client = clientRepo.findById(username);
ClientEntity client = clientRepo.findByRealmAndName(realmService.requireByKey(usernamePasswordAuthenticationRequest.getRealmKey()), username);
if (client != null)
{
if (BcryptUtil.matches(credential, client.getSecret()))

View File

@ -0,0 +1,101 @@
package de.tavolio.auth.identityproviders;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.tavolio.Role;
import de.tavolio.auth.utils.JwtUtils;
import de.tavolio.oidc.IssuerService;
import de.tavolio.auth.request.JWTRequest;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.client.ClientService;
import de.tavolio.realm.key.KeypairEntity;
import de.tavolio.realm.key.KeypairRepo;
import de.tavolio.realm.user.Permission;
import de.tavolio.verify.JwksService;
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.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.jwt.auth.principal.ParseException;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.infrastructure.Infrastructure;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.PublicKey;
import java.util.*;
@ApplicationScoped
@Priority(1)
public class ClientIdentityProvider implements IdentityProvider<JWTRequest>
{
private static final Logger LOG = LoggerFactory.getLogger(ClientIdentityProvider.class);
@Inject
JwksService jwksService;
@Inject
KeypairRepo keypairRepo;
@Inject
IssuerService issuerService;
@Inject
ClientService clientService;
@Override
public Class<JWTRequest> getRequestType()
{
return JWTRequest.class;
}
@Override
@ActivateRequestContext
public Uni<SecurityIdentity> authenticate(JWTRequest tokenAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext)
{
String raw = tokenAuthenticationRequest.getToken();
return Uni.createFrom().item(() -> {
String kid = JwtUtils.parseHeader(raw).getKid();
KeypairEntity keypairEntity = keypairRepo.findById(kid);
if (keypairEntity != null)
{
PublicKey publicKey = jwksService.findByKid(keypairEntity).toPublicKey();
RealmEntity realm = keypairEntity.getRealm();
try
{
JsonWebToken token = new DefaultJWTParser(getContextForRealm(realm.getKey())).verify(raw, publicKey);
ClientEntity client = clientService.requireByNameAndRealm(token.getName(), realm);
if (client != null)
{
Set<Permission> permissions = new HashSet<>(client.getPermissions());
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(client.getId())).addRole(Role.CLIENT.toString()).addAttribute("permissions", permissions).build();
}
}
catch (ParseException e)
{
throw new RuntimeException(e);
}
}
LOG.error("Cannot find key with id {}", kid);
throw new AuthenticationFailedException();
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool());
}
private JWTAuthContextInfo getContextForRealm(String realmKey)
{
JWTAuthContextInfo info = new JWTAuthContextInfo();
info.setIssuedBy(issuerService.getIssuer(realmKey));
return info;
}
}

View File

@ -0,0 +1,39 @@
package de.tavolio.auth.request;
import io.quarkus.security.identity.request.AuthenticationRequest;
import java.util.Map;
public class JWTRequest extends RealmScoped implements AuthenticationRequest
{
private final String token;
public JWTRequest(String realmKey, String token)
{
super(realmKey);
this.token = token;
}
public String getToken()
{
return token;
}
@Override
public <T> T getAttribute(String name)
{
return null;
}
@Override
public void setAttribute(String name, Object value)
{
}
@Override
public Map<String, Object> getAttributes()
{
return Map.of();
}
}

View File

@ -0,0 +1,16 @@
package de.tavolio.auth.request;
public class RealmScoped
{
private final String realmKey;
public RealmScoped(String realmKey)
{
this.realmKey = realmKey;
}
public String getRealmKey()
{
return realmKey;
}
}

View File

@ -0,0 +1,46 @@
package de.tavolio.auth.request;
import io.quarkus.security.identity.request.AuthenticationRequest;
import java.util.Map;
public class UsernamePasswordRequest extends RealmScoped implements AuthenticationRequest
{
private final String username;
private final String password;
public UsernamePasswordRequest(String realmKey, String username, String password)
{
super(realmKey);
this.username = username;
this.password = password;
}
public String getUsername()
{
return username;
}
public String getPassword()
{
return password;
}
@Override
public <T> T getAttribute(String name)
{
return null;
}
@Override
public void setAttribute(String name, Object value)
{
}
@Override
public Map<String, Object> getAttributes()
{
return Map.of();
}
}

View File

@ -0,0 +1,17 @@
package de.tavolio.auth.utils;
public class JwtHeader
{
public String kid;
public String getKid()
{
return kid;
}
public JwtHeader setKid(String kid)
{
this.kid = kid;
return this;
}
}

View File

@ -0,0 +1,32 @@
package de.tavolio.auth.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.security.AuthenticationFailedException;
import java.util.Base64;
import java.util.Map;
public class JwtUtils
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static JwtHeader parseHeader(String token)
{
String[] sections = token.split("\\.");
if (sections.length == 3)
{
try
{
Map<String, Object> result = OBJECT_MAPPER.readValue(new String(Base64.getUrlDecoder().decode(sections[0])), new TypeReference<Map<String, Object>>(){});
return new JwtHeader().setKid((String) result.get("kid"));
}
catch (JsonProcessingException e)
{
throw new RuntimeException(e);
}
}
throw new AuthenticationFailedException();
}
}

View File

@ -0,0 +1,6 @@
package de.tavolio.bootstrap;
public interface Bootstrapper<O>
{
void run(String key, O object);
}

View File

@ -39,7 +39,8 @@ public class ClientBootstrapper
.setSecret(Credentials.resolve(client.secret()))
.setRedirectURI(client.redirectURI())
.setRealm(realm)
.setPermissions(client.permissions());
.setPermissions(client.permissions())
.setAllowedGrants(client.allowedGrants());
clientRepo.persist(entity);
}
}

View File

@ -4,7 +4,6 @@ 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;
@ -17,9 +16,6 @@ import java.util.UUID;
@ApplicationScoped
public class RealmBootstrapService
{
@Inject
RealmService realmService;
@Inject
KeyBootstrapper keyBootstrapper;
@ -34,6 +30,8 @@ public class RealmBootstrapService
@Inject
AudienceStrategyRepo audienceStrategyRepo;
@Inject
RoleBootstrapper roleBootstrapper;
public void bootstrap(Map.Entry<String, Realm> realmEntry)
{
@ -42,6 +40,7 @@ public class RealmBootstrapService
keyBootstrapper.bootstrap(realm, realmValue.key());
clientBootstrapper.bootstrap(realm, realmValue.clients());
userBootstrapper.bootstrap(realm, realmValue.users());
roleBootstrapper.run(realm, realmValue.roles());
}
public RealmEntity run(String key, Realm realm)
@ -53,7 +52,7 @@ public class RealmBootstrapService
}
else
{
RealmEntity newRealm = new RealmEntity().setKey(key).setName(realm.name());
RealmEntity newRealm = new RealmEntity().setKey(key).setName(realm.name()).setLifetime(Optional.ofNullable(realm.lifetime()).orElse(60 * 30));
realmRepo.persist(newRealm);
bootstrapAudience(newRealm, realm.audience());
return newRealm;

View File

@ -0,0 +1,38 @@
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.List;
import java.util.UUID;
@ApplicationScoped
public class RoleBootstrapper
{
@Inject
RoleRepo roleRepo;
public void run(RealmEntity realm, List<Role> roles)
{
for (Role role : roles)
{
run(realm, role);
}
}
private void run(RealmEntity realm, Role role)
{
RoleEntity existingRole = roleRepo.findByRealmAndName(realm, role.name());
if (existingRole == null)
{
RoleEntity newRole = new RoleEntity().setId(UUID.randomUUID().toString());
newRole.setName(role.name());
newRole.setRealm(realm);
roleRepo.persist(newRole);
}
}
}

View File

@ -1,12 +1,13 @@
package de.tavolio.bootstrap.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.tavolio.realm.client.Grant;
import de.tavolio.realm.user.Permission;
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)
public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uri") String redirectURI, @JsonProperty("permissions") Set<String> permissionList, @JsonProperty("allowed-grants") Set<Grant> allowedGrants)
{
public Set<Permission> permissions()
{

View File

@ -3,6 +3,6 @@ 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<User> users)
public record Realm(String name, Key key, Integer lifetime, Audience audience, Map<String, Client> clients, List<Role> roles, List<String> permissions, List<User> users)
{
}

View File

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

View File

@ -1,30 +0,0 @@
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

@ -9,8 +9,9 @@ import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmService;
import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.client.ClientService;
import de.tavolio.realm.client.Grant;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@ -46,6 +47,9 @@ public class OidcResource
@Inject
RealmService realmService;
@Inject
SecurityIdentity identity;
@GET
@Path("/certs")
public Map<String, Object> certs()
@ -58,7 +62,7 @@ public class OidcResource
public Response auth(@QueryParam("client_id") String clientId, @FormParam("email") String email, @FormParam("password") String password)
{
RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByIdAndRealm(clientId, realm);
ClientEntity client = clientService.requireByNameAndRealm(clientId, realm);
if (client != null)
{
String code = authorizationService.generateBySessionCreation(realmKey, clientId, new AuthorizationCreation(email, password));
@ -73,14 +77,36 @@ public class OidcResource
@Transactional
public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("code") String code) throws NoSuchAlgorithmException, InvalidKeySpecException
{
if (GrantType.AUTH_CODE.equals(GrantType.fromValue(grantType)))
Grant grant = getGrant(grantType);
RealmEntity realm = realmService.getCurrent();
ClientEntity client = clientService.requireByNameAndRealm(identity.getPrincipal().getName(), realm);
if (!client.getAllowedGrants().contains(grant))
{
throw new ForbiddenException();
}
if (Grant.AUTHORIZATION_CODE.equals(grant))
{
return userTokenService.getToken(realmKey, code);
}
if (GrantType.CLIENT_CREDENTIALS.equals(GrantType.fromValue(grantType)))
if (Grant.CLIENT_CREDENTIALS.equals(grant))
{
return clientTokenService.getToken(realmKey);
}
throw new RuntimeException();
}
private Grant getGrant(String grantType)
{
if ("authorization_code".equals(grantType))
{
return Grant.AUTHORIZATION_CODE;
}
if ("client_credentials".equals(grantType))
{
return Grant.CLIENT_CREDENTIALS;
}
throw new RuntimeException();
}
}

View File

@ -39,7 +39,7 @@ public class AuthorizationService
{
RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByIdAndRealm(clientId, realm);
ClientEntity client = clientService.requireByNameAndRealm(clientId, realm);
Optional<UserEntity> accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, authorizationCreation.email());
if (accountEntityOptional.isPresent())
{

View File

@ -1,127 +0,0 @@
package de.tavolio.oidc.identityproviders;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.tavolio.Role;
import de.tavolio.oidc.IssuerService;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.client.ClientService;
import de.tavolio.realm.key.KeypairEntity;
import de.tavolio.realm.key.KeypairRepo;
import de.tavolio.realm.user.Permission;
import de.tavolio.verify.JwksService;
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.jwt.auth.principal.DefaultJWTParser;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.jwt.auth.principal.ParseException;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.infrastructure.Infrastructure;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.PublicKey;
import java.util.*;
@ApplicationScoped
@Priority(1)
public class ClientIdentityProvider implements IdentityProvider<TokenAuthenticationRequest>
{
private static final Logger LOG = LoggerFactory.getLogger(ClientIdentityProvider.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Inject
JwksService jwksService;
@Inject
KeypairRepo keypairRepo;
@Inject
IssuerService issuerService;
@Inject
ClientService clientService;
@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(() -> {
KeypairEntity keypairEntity = keypairRepo.findById(keyId);
if (keypairEntity != null)
{
PublicKey publicKey = jwksService.findByKid(keypairEntity).toPublicKey();
RealmEntity realm = keypairEntity.getRealm();
try
{
JsonWebToken token = new DefaultJWTParser(getContextForRealm(realm.getKey())).verify(raw, publicKey);
ClientEntity client = clientService.requireByIdAndRealm(token.getName(), realm);
if (client != null)
{
Set<Permission> permissions = new HashSet<>(client.getPermissions());
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(client.getId())).addRole(Role.CLIENT.toString()).addAttribute("permissions", permissions).build();
}
}
catch (ParseException e)
{
throw new RuntimeException(e);
}
}
LOG.error("Cannot find key with id {}", kid);
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.getUrlDecoder().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();
}
private JWTAuthContextInfo getContextForRealm(String realmKey)
{
JWTAuthContextInfo info = new JWTAuthContextInfo();
info.setIssuedBy(issuerService.getIssuer(realmKey));
return info;
}
}

View File

@ -32,13 +32,13 @@ public class ClientTokenService
public TokenResponse getToken(String realmKey)
{
RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByIdAndRealm(identity.getPrincipal().getName(), realm);
ClientEntity client = clientService.requireByNameAndRealm(identity.getPrincipal().getName(), realm);
if (client != null)
{
KeypairEntity keypair = realm.getKeys().getFirst();
PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair);
ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1);
String token = clientTokenGenerator.generateAccessToken(realmKey, client.getId(), expiresAt, signingKey, keypair.getId());
String token = clientTokenGenerator.generateAccessToken(realmKey, client.getName(), expiresAt, signingKey, keypair.getId());
return new TokenResponse().setAccessToken(token).setTokenType("Bearer").setExpiresAt(expiresAt.toInstant().getEpochSecond());
}
throw new BadRequestException();

View File

@ -47,11 +47,11 @@ public class UserTokenService
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().getId()))
if (entity != null && !ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getName()))
{
KeypairEntity keypair = realm.getKeys().getFirst();
PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair);
ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1);
ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(realm.getLifetime());
TokenResponse response = new TokenResponse()
.setAccessToken(userTokenGenerator.generateAccessToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId()))
.setRefreshToken(UUID.randomUUID().toString())

View File

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

View File

@ -6,6 +6,7 @@ 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.enterprise.inject.Vetoed;
import jakarta.persistence.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
@ -15,6 +16,7 @@ import java.util.List;
@Entity
@Table(name = "realm")
@Vetoed
public class RealmEntity
{
@Id
@ -26,6 +28,8 @@ public class RealmEntity
@Column(name = "realm_name")
private String name;
private int lifetime;
@OneToMany(mappedBy = "realm")
@OnDelete(action = OnDeleteAction.CASCADE)
private List<KeypairEntity> keys = new ArrayList<>();
@ -83,6 +87,17 @@ public class RealmEntity
return this;
}
public int getLifetime()
{
return lifetime;
}
public RealmEntity setLifetime(int lifetime)
{
this.lifetime = lifetime;
return this;
}
public List<KeypairEntity> getKeys()
{
return keys;

View File

@ -0,0 +1,16 @@
package de.tavolio.realm;
import java.util.List;
public class RealmMapper
{
public static List<Realm> map(List<RealmEntity> entities)
{
return entities.stream().map(RealmMapper::map).toList();
}
public static Realm map(RealmEntity entity)
{
return new Realm(entity.getKey(), entity.getName(), entity.getLifetime());
}
}

View File

@ -3,6 +3,8 @@ package de.tavolio.realm;
import de.tavolio.Role;
import de.tavolio.oidc.OidcConfigurationResource;
import de.tavolio.oidc.OidcResource;
import de.tavolio.realm.assignment.Assignment;
import de.tavolio.realm.assignment.AssignmentResource;
import de.tavolio.realm.client.ClientResource;
import de.tavolio.realm.role.RoleResource;
import de.tavolio.realm.user.UserResource;
@ -11,6 +13,7 @@ import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import java.util.List;
@ -21,19 +24,28 @@ public class RealmResource
@Inject
RealmRepo repo;
@Inject
RealmService realmService;
@Path("/{realm-key}")
public RealmSubResource realms()
{
return CDI.current().select(RealmSubResource.class).get();
}
@RolesAllowed("ROOT")
@RolesAllowed({"ROOT", "CLIENT"})
@Path("/{realm-key}/users")
public UserResource accounts()
{
return CDI.current().select(UserResource.class).get();
}
@Path("/{realm-key}/users/{user-id}/role-assignments")
public AssignmentResource assignments()
{
return CDI.current().select(AssignmentResource.class).get();
}
@RolesAllowed("ROOT")
@Path("/{realm-key}/clients")
public ClientResource clients()
@ -63,6 +75,13 @@ public class RealmResource
@GET
public List<Realm> get()
{
return repo.listAll().stream().map(item -> new Realm(item.getKey(), item.getName())).toList();
return RealmMapper.map(repo.listAll());
}
@POST
@RolesAllowed("ROOT")
public Realm post(RealmCreation realmCreation)
{
return realmService.create(realmCreation);
}
}

View File

@ -1,11 +1,13 @@
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.InternalServerErrorException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@ -22,6 +24,9 @@ public class RealmService
@Inject
RealmRepo repo;
@Inject
UriInfo context;
public RealmEntity requireByKey(String key)
{
RealmEntity realm = repo.findByKey(key);
@ -32,13 +37,23 @@ public class RealmService
throw new NotFoundException();
}
public RealmEntity getCurrent()
{
String realmKey = context.getPathParameters().getFirst("realm-key");
if (!StringUtils.isBlank(realmKey))
{
return requireByKey(realmKey);
}
throw new InternalServerErrorException();
}
@Transactional
public RealmEntity create(RealmCreation creation)
public Realm create(RealmCreation creation)
{
RealmEntity realm = new RealmEntity();
realm.setKey(creation.key());
realm.setName(creation.name());
repo.persist(realm);
return realm;
return RealmMapper.map(realm);
}
}

View File

@ -4,16 +4,15 @@ import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PathParam;
@RequestScoped
public class RealmSubResource
{
@PathParam("realm-key")
String key;
@Inject
RealmRepo repo;
@ -23,14 +22,14 @@ public class RealmSubResource
@GET
public Realm get()
{
RealmEntity realm = service.requireByKey(key);
return new Realm(realm.getKey(), realm.getName());
return RealmMapper.map(service.getCurrent());
}
@RolesAllowed("ROOT")
@DELETE
@Transactional
public void delete()
{
repo.delete(service.requireByKey(key));
repo.delete(service.getCurrent());
}
}

View File

@ -0,0 +1,9 @@
package de.tavolio.realm.assignment;
import de.tavolio.realm.role.Role;
import java.util.List;
public record Assignment(List<Role> assigned, List<Role> unassigned)
{
}

View File

@ -0,0 +1,54 @@
package de.tavolio.realm.assignment;
import de.tavolio.realm.role.RoleEntity;
import de.tavolio.realm.user.UserEntity;
import jakarta.persistence.*;
@Entity
@Table(name = "assignment")
public class AssignmentEntity
{
@Id
private String id;
@ManyToOne
@JoinColumn(name = "user_id")
private UserEntity user;
@ManyToOne
@JoinColumn(name = "role_id")
private RoleEntity role;
public String getId()
{
return id;
}
public AssignmentEntity setId(String id)
{
this.id = id;
return this;
}
public UserEntity getUser()
{
return user;
}
public AssignmentEntity setUser(UserEntity user)
{
this.user = user;
return this;
}
public RoleEntity getRole()
{
return role;
}
public AssignmentEntity setRole(RoleEntity role)
{
this.role = role;
return this;
}
}

View File

@ -0,0 +1,19 @@
package de.tavolio.realm.assignment;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.role.RoleEntity;
import de.tavolio.realm.user.UserEntity;
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 AssignmentRepo implements PanacheRepositoryBase<AssignmentEntity, String>
{
public AssignmentEntity findByRoleAndUser(RoleEntity role, UserEntity user)
{
return find("role = :role AND user = :user", Parameters.with("role", role).and("user", user)).firstResult();
}
}

View File

@ -0,0 +1,7 @@
package de.tavolio.realm.assignment;
import java.util.List;
public record AssignmentRequest(List<String> assign, List<String> unassign)
{
}

View File

@ -0,0 +1,34 @@
package de.tavolio.realm.assignment;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import java.util.List;
@ApplicationScoped
public class AssignmentResource
{
@Inject
AssignmentService assignmentService;
@GET
public Assignment get(@PathParam("user-id") String userId)
{
return assignmentService.get(userId);
}
@PUT
public void post(@PathParam("user-id") String userId, AssignmentRequest request)
{
if (request.assign() != null)
{
assignmentService.assign(userId, request.assign());
}
if (request.unassign() != null)
{
assignmentService.unassign(userId, request.unassign());
}
}
}

View File

@ -0,0 +1,98 @@
package de.tavolio.realm.assignment;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmService;
import de.tavolio.realm.role.Role;
import de.tavolio.realm.role.RoleEntity;
import de.tavolio.realm.role.RoleRepo;
import de.tavolio.realm.role.RoleService;
import de.tavolio.realm.user.UserEntity;
import de.tavolio.realm.user.UserRepo;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@ApplicationScoped
public class AssignmentService
{
@Inject
RoleRepo roleRepo;
@Inject
RealmService realmService;
@Inject
UserRepo userRepo;
@Inject
AssignmentRepo assignmentRepo;
public Assignment get(String userId)
{
RealmEntity realm = realmService.getCurrent();
UserEntity user = userRepo.findByRealmAndId(realm, userId);
List<Role> assigned = new LinkedList<>();
List<Role> unassigned = new LinkedList<>();
for (RoleEntity role : roleRepo.findByRealm(realm))
{
if (isAssigned(user, role))
{
assigned.add(new Role(role.getId(), role.getName()));
}
else
{
unassigned.add(new Role(role.getId(), role.getName()));
}
}
return new Assignment(assigned, unassigned);
}
@Transactional
public void assign(String userId, List<String> roles)
{
RealmEntity realm = realmService.getCurrent();
UserEntity user = userRepo.findByRealmAndId(realm, userId);
for (String roleName : roles)
{
RoleEntity role = roleRepo.findByRealmAndName(realm, roleName);
assign(user, role);
}
}
public void assign(UserEntity user, RoleEntity role)
{
AssignmentEntity assignment = new AssignmentEntity().setId(UUID.randomUUID().toString());
assignment.setUser(user);
assignment.setRole(role);
assignmentRepo.persist(assignment);
}
@Transactional
public void unassign(String userId, List<String> roles)
{
RealmEntity realm = realmService.getCurrent();
UserEntity user = userRepo.findByRealmAndId(realm, userId);
for (String roleName : roles)
{
RoleEntity role = roleRepo.findByRealmAndName(realm, roleName);
AssignmentEntity assignment = assignmentRepo.findByRoleAndUser(role, user);
assignmentRepo.delete(assignment);
}
}
private boolean isAssigned(UserEntity user, RoleEntity role)
{
for (AssignmentEntity assignment : role.getAssignments())
{
if (assignment.getUser().getId().equals(user.getId()))
{
return true;
}
}
return false;
}
}

View File

@ -43,7 +43,7 @@ public class ClientEntity implements RealmScoped
@CollectionTable(name = "allowed_grants", joinColumns = @JoinColumn(name = "client_id"))
@Column(name = "grant_name")
@Enumerated(EnumType.STRING)
private Set<Grant> allowedGrants = new HashSet<Grant>();
private Set<Grant> allowedGrants = new HashSet<>();
public String getId()
{

View File

@ -1,13 +1,11 @@
package de.tavolio.realm.client;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmRepo;
import de.tavolio.realm.RealmService;
import io.quarkus.elytron.security.common.BcryptUtil;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.NotFoundException;
import org.apache.commons.lang3.StringUtils;
@ -22,9 +20,9 @@ public class ClientService
@Inject
RealmService realmService;
public ClientEntity requireByIdAndRealm(String clientId, RealmEntity realm)
public ClientEntity requireByNameAndRealm(String clientId, RealmEntity realm)
{
ClientEntity client = clientRepo.findByRealmAndId(realm, clientId);
ClientEntity client = clientRepo.findByRealmAndName(realm, clientId);
if (client != null)
{
return client;

View File

@ -2,5 +2,5 @@ package de.tavolio.realm.client;
public enum Grant
{
CLIENT_CREDENTIALS, AUTHORIZATION, PASSWORD
CLIENT_CREDENTIALS, AUTHORIZATION_CODE, PASSWORD
}

View File

@ -2,6 +2,7 @@ package de.tavolio.realm.role;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmScoped;
import de.tavolio.realm.assignment.AssignmentEntity;
import jakarta.persistence.*;
import java.util.ArrayList;
@ -21,6 +22,9 @@ public class RoleEntity implements RealmScoped
@JoinColumn(name = "realm_id")
private RealmEntity realm;
@OneToMany(mappedBy = "role")
private List<AssignmentEntity> assignments;
public String getId()
{
return id;
@ -53,4 +57,15 @@ public class RoleEntity implements RealmScoped
this.realm = realm;
return this;
}
public List<AssignmentEntity> getAssignments()
{
return assignments;
}
public RoleEntity setAssignments(List<AssignmentEntity> assignments)
{
this.assignments = assignments;
return this;
}
}

View File

@ -15,4 +15,9 @@ public class RoleRepo implements PanacheRepositoryBase<RoleEntity, String>
{
return list("realm = :realm", Parameters.with("realm", realm));
}
public RoleEntity findByRealmAndName(RealmEntity realm, String name)
{
return find("realm = :realm AND name = :name", Parameters.with("realm", realm).and("name", name)).firstResult();
}
}

View File

@ -2,5 +2,5 @@ package de.tavolio.realm.user;
public enum Permission
{
USER_VIEW, USER_CREATE
USER_VIEW, USER_DELETE, USER_CREATE
}

View File

@ -1,6 +1,7 @@
package de.tavolio.realm.user;
import de.tavolio.realm.RealmScoped;
import de.tavolio.realm.assignment.AssignmentEntity;
import de.tavolio.realm.code.CodeEntity;
import de.tavolio.realm.RealmEntity;
import jakarta.persistence.*;
@ -34,6 +35,9 @@ public class UserEntity implements RealmScoped
@OneToMany(mappedBy = "account")
private List<CodeEntity> codes;
@OneToMany(mappedBy = "user")
private List<AssignmentEntity> assignments;
public static UserEntity init()
{
return new UserEntity().setId(UUID.randomUUID().toString());
@ -126,4 +130,15 @@ public class UserEntity implements RealmScoped
this.codes = codes;
return this;
}
public List<AssignmentEntity> getAssignments()
{
return assignments;
}
public UserEntity setAssignments(List<AssignmentEntity> assignments)
{
this.assignments = assignments;
return this;
}
}

View File

@ -53,7 +53,7 @@ public class UserResource
@Path("/{id}")
public void delete(@PathParam("id") String id)
{
permissionService.hasPermission(Permission.USER_VIEW);
permissionService.hasPermission(Permission.USER_DELETE);
userService.delete(realmKey, id);
}

View File

@ -25,7 +25,7 @@ quarkus.flyway.enabled=false
quarkus.flyway.migrate-at-start=true
quarkus.http.access-log.enabled=true
quarkus.http.auth.basic=true
quarkus.http.auth.basic=false
%dev.io.verifoo.http.origin=http://localhost:8089

View File

@ -8,17 +8,22 @@ realms:
key:
type: EC
alg: P256
roles:
- name: ADMIN
- name: USER
clients:
backend:
secret:
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
redirect-uri: http://localhost:8080/callback
redirect-uri: http://localhost:8080/auth/callback
permissions:
- USER_VIEW
allowed-grants:
- AUTHORIZATION_CODE
analytics-backend:
secret:
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
redirect-uri: http://localhost:8080/callback
redirect-uri: http://localhost:8080/auth/callback
permissions:
- USER_VIEW
users: