From 7d638e653096ad59004092d15874536f3ad22881 Mon Sep 17 00:00:00 2001 From: Andreas Dinauer Date: Sat, 14 Mar 2026 18:54:50 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Transform=20to=20IDP=20pro?= =?UTF-8?q?vider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bootstrap/AccountBootstrapper.java | 59 -------- .../tavolio/bootstrap/ClientBootstrapper.java | 20 ++- .../de/tavolio/bootstrap/Credentials.java | 36 +++++ .../bootstrap/RealmBootstrapService.java | 8 +- .../tavolio/bootstrap/RoleBootstrapper.java | 43 ------ .../bootstrap/SuperuserBootstrapper.java | 4 +- .../tavolio/bootstrap/UserBootstrapper.java | 48 +++++++ .../de/tavolio/bootstrap/model/Account.java | 7 - .../de/tavolio/bootstrap/model/Client.java | 10 +- .../tavolio/bootstrap/model/Credential.java | 5 + .../de/tavolio/bootstrap/model/Realm.java | 2 +- .../java/de/tavolio/bootstrap/model/User.java | 7 + .../java/de/tavolio/oidc/IssuerService.java | 2 +- .../oidc/OidcConfigurationResource.java | 2 +- .../java/de/tavolio/oidc/OidcResource.java | 20 +-- .../AuthorizationService.java | 10 +- .../model/AuthorizationCreation.java} | 4 +- .../BasicAuthIdentityProvider.java} | 8 +- .../ClientIdentityProvider.java | 129 ++++++++++++++++++ .../oidc/token/ClientTokenGenerator.java | 29 ++++ .../oidc/token/ClientTokenService.java | 48 +++++++ ...Generator.java => UserTokenGenerator.java} | 2 +- ...okenService.java => UserTokenService.java} | 29 ++-- .../de/tavolio/realm/PermissionService.java | 34 +++++ src/main/java/de/tavolio/realm/Realm.java | 5 + .../java/de/tavolio/realm/RealmResource.java | 6 + .../de/tavolio/realm/RealmSubResource.java | 25 ++++ .../de/tavolio/realm/client/ClientEntity.java | 23 ++++ .../tavolio/realm/client/ClientService.java | 11 +- .../de/tavolio/realm/key/KeypairEntity.java | 20 +++ .../de/tavolio/realm/key/KeypairService.java | 4 +- .../de/tavolio/realm/user/Permission.java | 6 + .../de/tavolio/realm/user/UserResource.java | 19 ++- .../java/de/tavolio/verify/JwksService.java | 46 ++----- .../java/de/tavolio/verify/TokenVerifier.java | 81 ----------- src/main/resources/application.properties | 25 +--- src/main/resources/bootstrap.yaml | 20 +-- 37 files changed, 531 insertions(+), 326 deletions(-) delete mode 100644 src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java create mode 100644 src/main/java/de/tavolio/bootstrap/Credentials.java delete mode 100644 src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java create mode 100644 src/main/java/de/tavolio/bootstrap/UserBootstrapper.java delete mode 100644 src/main/java/de/tavolio/bootstrap/model/Account.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/Credential.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/User.java rename src/main/java/de/tavolio/oidc/{session => auth}/AuthorizationService.java (84%) rename src/main/java/de/tavolio/oidc/{session/dto/SessionCreation.java => auth/model/AuthorizationCreation.java} (67%) rename src/main/java/de/tavolio/oidc/{auth/OidcClientIdentityProvider.java => identityproviders/BasicAuthIdentityProvider.java} (93%) create mode 100644 src/main/java/de/tavolio/oidc/identityproviders/ClientIdentityProvider.java create mode 100644 src/main/java/de/tavolio/oidc/token/ClientTokenGenerator.java create mode 100644 src/main/java/de/tavolio/oidc/token/ClientTokenService.java rename src/main/java/de/tavolio/oidc/token/{TokenGenerator.java => UserTokenGenerator.java} (97%) rename src/main/java/de/tavolio/oidc/token/{TokenService.java => UserTokenService.java} (64%) create mode 100644 src/main/java/de/tavolio/realm/PermissionService.java create mode 100644 src/main/java/de/tavolio/realm/Realm.java create mode 100644 src/main/java/de/tavolio/realm/RealmSubResource.java create mode 100644 src/main/java/de/tavolio/realm/user/Permission.java delete mode 100644 src/main/java/de/tavolio/verify/TokenVerifier.java diff --git a/src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java b/src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java deleted file mode 100644 index c0c3e4b..0000000 --- a/src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java +++ /dev/null @@ -1,59 +0,0 @@ -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 accounts) - { - for (Account account : accounts) - { - run(realm, account); - } - } - - public void run(RealmEntity realm, Account account) - { - Optional 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; - } -} diff --git a/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java b/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java index 4ddc35a..722a6e1 100644 --- a/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java +++ b/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java @@ -9,6 +9,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import java.util.Map; +import java.util.Optional; @ApplicationScoped public class ClientBootstrapper @@ -23,8 +24,23 @@ public class ClientBootstrapper { for (Map.Entry clientEntry : clients.entrySet()) { - ClientEntity client = clientService.findOrCreate(realm, clientEntry); - clientRepo.persist(client); + run(realm, clientEntry); + } + } + + private void run(RealmEntity realm, Map.Entry bootstrap) + { + Optional existingClient = clientRepo.findByRealmAndIdOptional(realm, bootstrap.getKey()); + if (existingClient.isEmpty()) + { + String id = bootstrap.getKey(); + Client client = bootstrap.getValue(); + ClientEntity entity = new ClientEntity().setId(id) + .setSecret(Credentials.resolve(client.secret())) + .setRedirectURI(client.redirectURI()) + .setRealm(realm) + .setPermissions(client.permissions()); + clientRepo.persist(entity); } } } diff --git a/src/main/java/de/tavolio/bootstrap/Credentials.java b/src/main/java/de/tavolio/bootstrap/Credentials.java new file mode 100644 index 0000000..c072e90 --- /dev/null +++ b/src/main/java/de/tavolio/bootstrap/Credentials.java @@ -0,0 +1,36 @@ +package de.tavolio.bootstrap; + +import de.tavolio.bootstrap.model.Credential; +import de.tavolio.bootstrap.model.User; +import io.quarkus.elytron.security.common.BcryptUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; + +public class Credentials +{ + public static String resolve(Credential credential) + { + if (!StringUtils.isBlank(credential.bcrypt())) + { + if (Strings.CI.startsWithAny(credential.bcrypt(), "$2a$", "$2b$", "$2y$")) + { + return credential.bcrypt(); + } + throw new IllegalStateException(); + } + if (!StringUtils.isBlank(credential.env())) + { + String value = System.getenv(credential.env()); + if (!StringUtils.isBlank(value)) + { + return BcryptUtil.bcryptHash(value); + } + throw new IllegalStateException(); + } + if (!StringUtils.isBlank(credential.plain())) + { + return BcryptUtil.bcryptHash(credential.plain()); + } + throw new IllegalArgumentException("Cannot find password source."); + } +} diff --git a/src/main/java/de/tavolio/bootstrap/RealmBootstrapService.java b/src/main/java/de/tavolio/bootstrap/RealmBootstrapService.java index ab63e20..1b60841 100644 --- a/src/main/java/de/tavolio/bootstrap/RealmBootstrapService.java +++ b/src/main/java/de/tavolio/bootstrap/RealmBootstrapService.java @@ -26,14 +26,11 @@ public class RealmBootstrapService @Inject ClientBootstrapper clientBootstrapper; - @Inject - RoleBootstrapper roleBootstrapper; - @Inject RealmRepo realmRepo; @Inject - AccountBootstrapper accountBootstrapper; + UserBootstrapper userBootstrapper; @Inject AudienceStrategyRepo audienceStrategyRepo; @@ -42,10 +39,9 @@ public class RealmBootstrapService { 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()); + userBootstrapper.bootstrap(realm, realmValue.users()); } public RealmEntity run(String key, Realm realm) diff --git a/src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java b/src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java deleted file mode 100644 index d58f31d..0000000 --- a/src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java +++ /dev/null @@ -1,43 +0,0 @@ -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 roles) - { - for (Map.Entry roleEntry : roles.entrySet()) - { - run(realm, roleEntry); - } - } - - public void run(RealmEntity realm, Map.Entry 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)); - } -} diff --git a/src/main/java/de/tavolio/bootstrap/SuperuserBootstrapper.java b/src/main/java/de/tavolio/bootstrap/SuperuserBootstrapper.java index 97d62f7..3985ce9 100644 --- a/src/main/java/de/tavolio/bootstrap/SuperuserBootstrapper.java +++ b/src/main/java/de/tavolio/bootstrap/SuperuserBootstrapper.java @@ -20,8 +20,8 @@ public class SuperuserBootstrapper 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); + String username = config.getValue("io.verifoo.superuser.username", String.class); + String password = config.getValue("io.verifoo.superuser.password", String.class); if (!StringUtils.isBlank(username) && !StringUtils.isBlank(password) && superuserRepo.count() == 0) { SuperuserEntity superuser = new SuperuserEntity(); diff --git a/src/main/java/de/tavolio/bootstrap/UserBootstrapper.java b/src/main/java/de/tavolio/bootstrap/UserBootstrapper.java new file mode 100644 index 0000000..e747468 --- /dev/null +++ b/src/main/java/de/tavolio/bootstrap/UserBootstrapper.java @@ -0,0 +1,48 @@ +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.User; +import de.tavolio.realm.RealmEntity; +import io.quarkus.elytron.security.common.BcryptUtil; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class UserBootstrapper +{ + @Inject + UserRepo userRepo; + + public void bootstrap(RealmEntity realm, List users) + { + for (User user : users) + { + run(realm, user); + } + } + + public void run(RealmEntity realm, User user) + { + Optional existingAccount = userRepo.findOptionalByRealmAndEmail(realm, user.email()); + if (existingAccount.isEmpty()) + { + UserEntity newAccount = new UserEntity(); + newAccount.setId(UUID.randomUUID().toString()); + newAccount.setEmail(user.email()); + newAccount.setFirstname(user.firstname()); + newAccount.setLastname(user.lastname()); + newAccount.setRealm(realm); + newAccount.setStatus(UserStatus.INIT); + newAccount.setPassword(Credentials.resolve(user.password())); + userRepo.persist(newAccount); + } + } +} diff --git a/src/main/java/de/tavolio/bootstrap/model/Account.java b/src/main/java/de/tavolio/bootstrap/model/Account.java deleted file mode 100644 index 1e37fb6..0000000 --- a/src/main/java/de/tavolio/bootstrap/model/Account.java +++ /dev/null @@ -1,7 +0,0 @@ -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) -{ -} \ No newline at end of file diff --git a/src/main/java/de/tavolio/bootstrap/model/Client.java b/src/main/java/de/tavolio/bootstrap/model/Client.java index b45acbe..a0622e4 100644 --- a/src/main/java/de/tavolio/bootstrap/model/Client.java +++ b/src/main/java/de/tavolio/bootstrap/model/Client.java @@ -1,9 +1,15 @@ package de.tavolio.bootstrap.model; import com.fasterxml.jackson.annotation.JsonProperty; +import de.tavolio.realm.user.Permission; -import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; -public record Client(@JsonProperty("client-secret") String clientSecret, @JsonProperty("redirect-uri") String redirectURI, List roles) +public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uri") String redirectURI, @JsonProperty("permissions") Set permissionList) { + public Set permissions() + { + return permissionList.stream().map(item -> Permission.valueOf(item.toUpperCase())).collect(Collectors.toSet()); + } } \ No newline at end of file diff --git a/src/main/java/de/tavolio/bootstrap/model/Credential.java b/src/main/java/de/tavolio/bootstrap/model/Credential.java new file mode 100644 index 0000000..b62c8c3 --- /dev/null +++ b/src/main/java/de/tavolio/bootstrap/model/Credential.java @@ -0,0 +1,5 @@ +package de.tavolio.bootstrap.model; + +public record Credential(String plain, String bcrypt, String env) +{ +} diff --git a/src/main/java/de/tavolio/bootstrap/model/Realm.java b/src/main/java/de/tavolio/bootstrap/model/Realm.java index 89d5141..07b97d6 100644 --- a/src/main/java/de/tavolio/bootstrap/model/Realm.java +++ b/src/main/java/de/tavolio/bootstrap/model/Realm.java @@ -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 clients, Map roles, List permissions, List accounts) +public record Realm(String name, Key key, Audience audience, Map clients, Map roles, List permissions, List users) { } diff --git a/src/main/java/de/tavolio/bootstrap/model/User.java b/src/main/java/de/tavolio/bootstrap/model/User.java new file mode 100644 index 0000000..3d9c74f --- /dev/null +++ b/src/main/java/de/tavolio/bootstrap/model/User.java @@ -0,0 +1,7 @@ +package de.tavolio.bootstrap.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record User(String email, @JsonProperty("first-name") String firstname, @JsonProperty("last-name") String lastname, Credential password) +{ +} \ No newline at end of file diff --git a/src/main/java/de/tavolio/oidc/IssuerService.java b/src/main/java/de/tavolio/oidc/IssuerService.java index c10e3b3..2b80007 100644 --- a/src/main/java/de/tavolio/oidc/IssuerService.java +++ b/src/main/java/de/tavolio/oidc/IssuerService.java @@ -6,7 +6,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; @ApplicationScoped public class IssuerService { - @ConfigProperty(name = "dev.dinauer.idp.origin") + @ConfigProperty(name = "io.verifoo.http.origin") String origin; public String getIssuer(String realmKey) diff --git a/src/main/java/de/tavolio/oidc/OidcConfigurationResource.java b/src/main/java/de/tavolio/oidc/OidcConfigurationResource.java index 5f9b863..1d1a68d 100644 --- a/src/main/java/de/tavolio/oidc/OidcConfigurationResource.java +++ b/src/main/java/de/tavolio/oidc/OidcConfigurationResource.java @@ -16,7 +16,7 @@ public class OidcConfigurationResource @Inject IssuerService issuerService; - @ConfigProperty(name = "dev.dinauer.idp.origin") + @ConfigProperty(name = "io.verifoo.http.origin") String origin; @GET diff --git a/src/main/java/de/tavolio/oidc/OidcResource.java b/src/main/java/de/tavolio/oidc/OidcResource.java index 1fae4c1..81a13e0 100644 --- a/src/main/java/de/tavolio/oidc/OidcResource.java +++ b/src/main/java/de/tavolio/oidc/OidcResource.java @@ -1,9 +1,10 @@ package de.tavolio.oidc; -import de.tavolio.oidc.session.AuthorizationService; -import de.tavolio.oidc.session.dto.SessionCreation; +import de.tavolio.oidc.auth.AuthorizationService; +import de.tavolio.oidc.auth.model.AuthorizationCreation; +import de.tavolio.oidc.token.ClientTokenService; import de.tavolio.oidc.token.model.TokenResponse; -import de.tavolio.oidc.token.TokenService; +import de.tavolio.oidc.token.UserTokenService; import jakarta.annotation.security.RolesAllowed; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; @@ -26,11 +27,14 @@ public class OidcResource JwksService jwksService; @Inject - TokenService tokenService; + UserTokenService userTokenService; @Inject AuthorizationService authorizationService; + @Inject + ClientTokenService clientTokenService; + @GET @Path("/certs") public Map certs() @@ -42,7 +46,7 @@ public class OidcResource @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)); + String code = authorizationService.generateBySessionCreation(realmKey, clientId, new AuthorizationCreation(email, password)); return Response.status(302).location(URI.create("http://localhost:8080/callback?code=" + code + "&state=d")).build(); } @@ -50,15 +54,15 @@ public class OidcResource @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 + public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("code") String code) throws NoSuchAlgorithmException, InvalidKeySpecException { if (GrantType.AUTH_CODE.equals(GrantType.fromValue(grantType))) { - return tokenService.getUserToken(realmKey, code); + return userTokenService.getToken(realmKey, code); } if (GrantType.CLIENT_CREDENTIALS.equals(GrantType.fromValue(grantType))) { - return tokenService.getClientToken(realmKey); + return clientTokenService.getToken(realmKey); } throw new RuntimeException(); } diff --git a/src/main/java/de/tavolio/oidc/session/AuthorizationService.java b/src/main/java/de/tavolio/oidc/auth/AuthorizationService.java similarity index 84% rename from src/main/java/de/tavolio/oidc/session/AuthorizationService.java rename to src/main/java/de/tavolio/oidc/auth/AuthorizationService.java index 87450dd..a98e9e7 100644 --- a/src/main/java/de/tavolio/oidc/session/AuthorizationService.java +++ b/src/main/java/de/tavolio/oidc/auth/AuthorizationService.java @@ -1,4 +1,4 @@ -package de.tavolio.oidc.session; +package de.tavolio.oidc.auth; import de.tavolio.realm.RealmService; import de.tavolio.realm.user.UserEntity; @@ -8,7 +8,7 @@ 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 de.tavolio.oidc.auth.model.AuthorizationCreation; import io.quarkus.elytron.security.common.BcryptUtil; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -35,16 +35,16 @@ public class AuthorizationService ClientService clientService; @Transactional - public String generateBySessionCreation(String realmKey, String clientId, SessionCreation sessionCreation) + public String generateBySessionCreation(String realmKey, String clientId, AuthorizationCreation authorizationCreation) { RealmEntity realm = realmService.requireByKey(realmKey); ClientEntity client = clientService.findByIdAndRealm(clientId, realm); - Optional accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, sessionCreation.email()); + Optional accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, authorizationCreation.email()); if (accountEntityOptional.isPresent()) { UserEntity userEntity = accountEntityOptional.get(); - if (BcryptUtil.matches(sessionCreation.password(), userEntity.getPassword())) + if (BcryptUtil.matches(authorizationCreation.password(), userEntity.getPassword())) { CodeEntity code = new CodeEntity().setId(UUID.randomUUID().toString()); code.setExpiresAt(ZonedDateTime.now().plusMinutes(1)); diff --git a/src/main/java/de/tavolio/oidc/session/dto/SessionCreation.java b/src/main/java/de/tavolio/oidc/auth/model/AuthorizationCreation.java similarity index 67% rename from src/main/java/de/tavolio/oidc/session/dto/SessionCreation.java rename to src/main/java/de/tavolio/oidc/auth/model/AuthorizationCreation.java index 5f1c002..f4d7c84 100644 --- a/src/main/java/de/tavolio/oidc/session/dto/SessionCreation.java +++ b/src/main/java/de/tavolio/oidc/auth/model/AuthorizationCreation.java @@ -1,9 +1,9 @@ -package de.tavolio.oidc.session.dto; +package de.tavolio.oidc.auth.model; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -public record SessionCreation( +public record AuthorizationCreation( @Email String email, @NotBlank String password) { diff --git a/src/main/java/de/tavolio/oidc/auth/OidcClientIdentityProvider.java b/src/main/java/de/tavolio/oidc/identityproviders/BasicAuthIdentityProvider.java similarity index 93% rename from src/main/java/de/tavolio/oidc/auth/OidcClientIdentityProvider.java rename to src/main/java/de/tavolio/oidc/identityproviders/BasicAuthIdentityProvider.java index 7ba318e..5d67b71 100644 --- a/src/main/java/de/tavolio/oidc/auth/OidcClientIdentityProvider.java +++ b/src/main/java/de/tavolio/oidc/identityproviders/BasicAuthIdentityProvider.java @@ -1,4 +1,4 @@ -package de.tavolio.oidc.auth; +package de.tavolio.oidc.identityproviders; import de.tavolio.Role; import de.tavolio.realm.user.UserRepo; @@ -16,20 +16,20 @@ 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.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.control.ActivateRequestContext; import jakarta.inject.Inject; @ApplicationScoped -public class OidcClientIdentityProvider implements IdentityProvider +@Priority(1) +public class BasicAuthIdentityProvider implements IdentityProvider { @Inject ClientRepo clientRepo; @Inject SuperuserRepo superuserRepo; - @Inject - UserRepo userRepo; @Override public Class getRequestType() diff --git a/src/main/java/de/tavolio/oidc/identityproviders/ClientIdentityProvider.java b/src/main/java/de/tavolio/oidc/identityproviders/ClientIdentityProvider.java new file mode 100644 index 0000000..3bdd1e5 --- /dev/null +++ b/src/main/java/de/tavolio/oidc/identityproviders/ClientIdentityProvider.java @@ -0,0 +1,129 @@ +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.verify.JwksService; +import de.tavolio.verify.jwks.JwksKey; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.StringPermission; +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.Permission; +import java.security.PublicKey; +import java.util.*; +import java.util.stream.Collectors; + +@ApplicationScoped +@Priority(1) +public class ClientIdentityProvider implements IdentityProvider +{ + 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 getRequestType() + { + return TokenAuthenticationRequest.class; + } + + @Override + @ActivateRequestContext + public Uni 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.findByIdAndRealm(token.getName(), realm); + if (client != null) + { + return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(client.getId())).addRole(Role.CLIENT.toString()).addAttribute("permissions", new HashSet<>(client.getPermissions())).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 getHeader(String raw) throws JsonProcessingException + { + return OBJECT_MAPPER.readValue(new String(Base64.getUrlDecoder().decode(section(raw))), new TypeReference>(){}); + } + + 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; + } +} \ No newline at end of file diff --git a/src/main/java/de/tavolio/oidc/token/ClientTokenGenerator.java b/src/main/java/de/tavolio/oidc/token/ClientTokenGenerator.java new file mode 100644 index 0000000..e7c1476 --- /dev/null +++ b/src/main/java/de/tavolio/oidc/token/ClientTokenGenerator.java @@ -0,0 +1,29 @@ +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 ClientTokenGenerator +{ + @Inject + IssuerService issuerService; + + public String generateAccessToken(String realmKey, String clientId, ZonedDateTime expiresAt, PrivateKey key, String keyId) + { + return Jwt.claims() + .upn(clientId) + .audience(clientId) + .subject(clientId) + .claim("realm_key", realmKey) + .claim("client_id", clientId) + .expiresAt(expiresAt.toInstant()) + .issuer(issuerService.getIssuer(realmKey)).jws().keyId(keyId) + .sign(key); + } +} \ No newline at end of file diff --git a/src/main/java/de/tavolio/oidc/token/ClientTokenService.java b/src/main/java/de/tavolio/oidc/token/ClientTokenService.java new file mode 100644 index 0000000..5173e4f --- /dev/null +++ b/src/main/java/de/tavolio/oidc/token/ClientTokenService.java @@ -0,0 +1,48 @@ +package de.tavolio.oidc.token; + +import de.tavolio.oidc.token.model.TokenResponse; +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.key.KeypairEntity; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; + +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.time.ZonedDateTime; + +@ApplicationScoped +public class ClientTokenService +{ + @Inject + RealmService realmService; + + @Inject + ClientService clientService; + + @Inject + SecurityIdentity identity; + + @Inject + ClientTokenGenerator clientTokenGenerator; + + public TokenResponse getToken(String realmKey) + { + RealmEntity realm = realmService.requireByKey(realmKey); + ClientEntity client = clientService.findByIdAndRealm(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()); + return new TokenResponse().setAccessToken(token).setTokenType("Bearer").setExpiresAt(expiresAt.toInstant().getEpochSecond()); + } + throw new BadRequestException(); + } +} diff --git a/src/main/java/de/tavolio/oidc/token/TokenGenerator.java b/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java similarity index 97% rename from src/main/java/de/tavolio/oidc/token/TokenGenerator.java rename to src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java index cd5788b..0a10ba4 100644 --- a/src/main/java/de/tavolio/oidc/token/TokenGenerator.java +++ b/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java @@ -9,7 +9,7 @@ import java.security.PrivateKey; import java.time.ZonedDateTime; @ApplicationScoped -public class TokenGenerator +public class UserTokenGenerator { @Inject IssuerService issuerService; diff --git a/src/main/java/de/tavolio/oidc/token/TokenService.java b/src/main/java/de/tavolio/oidc/token/UserTokenService.java similarity index 64% rename from src/main/java/de/tavolio/oidc/token/TokenService.java rename to src/main/java/de/tavolio/oidc/token/UserTokenService.java index 6c9de0e..3e0e9a3 100644 --- a/src/main/java/de/tavolio/oidc/token/TokenService.java +++ b/src/main/java/de/tavolio/oidc/token/UserTokenService.java @@ -2,6 +2,8 @@ package de.tavolio.oidc.token; import de.tavolio.oidc.token.model.TokenResponse; import de.tavolio.realm.RealmRepo; +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; @@ -11,41 +13,31 @@ 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 +public class UserTokenService { @Inject CodeRepo codeRepo; @Inject - TokenGenerator tokenGenerator; + UserTokenGenerator userTokenGenerator; @Inject SecurityIdentity identity; @Inject - RealmRepo realmRepo; + RealmService realmService; - @POST - public TokenResponse getClientToken(String realmKey) - { - return null; - } - - public TokenResponse getUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException + public TokenResponse getToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException { return generateUserToken(realmKey, code); } @@ -53,18 +45,17 @@ public class TokenService private TokenResponse generateUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException { String principal = identity.getPrincipal().getName(); - RealmEntity realm = realmRepo.findById(realmKey); + RealmEntity realm = realmService.requireByKey(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()); + PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair); 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())) + .setAccessToken(userTokenGenerator.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())) + .setIdToken(userTokenGenerator.generateIDToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId())) .setTokenType("Bearer") .setExpiresAt(expiresAt.toInstant().getEpochSecond()); codeRepo.delete(entity); diff --git a/src/main/java/de/tavolio/realm/PermissionService.java b/src/main/java/de/tavolio/realm/PermissionService.java new file mode 100644 index 0000000..a6d937e --- /dev/null +++ b/src/main/java/de/tavolio/realm/PermissionService.java @@ -0,0 +1,34 @@ +package de.tavolio.realm; + +import de.tavolio.Role; +import de.tavolio.realm.user.Permission; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; + +import java.util.Set; + +@RequestScoped +public class PermissionService +{ + @Inject + SecurityIdentity identity; + + public void hasPermission(Permission... required) + { + if (identity.hasRole(Role.ROOT.toString())) + { + return; + } + Set granted = identity.getAttribute("permissions"); + for (Permission permission : required) + { + if (granted != null && granted.contains(permission)) + { + return; + } + } + throw new ForbiddenException(); + } +} diff --git a/src/main/java/de/tavolio/realm/Realm.java b/src/main/java/de/tavolio/realm/Realm.java new file mode 100644 index 0000000..95037bd --- /dev/null +++ b/src/main/java/de/tavolio/realm/Realm.java @@ -0,0 +1,5 @@ +package de.tavolio.realm; + +public record Realm(String key, String name) +{ +} diff --git a/src/main/java/de/tavolio/realm/RealmResource.java b/src/main/java/de/tavolio/realm/RealmResource.java index 4452088..6a74dfa 100644 --- a/src/main/java/de/tavolio/realm/RealmResource.java +++ b/src/main/java/de/tavolio/realm/RealmResource.java @@ -9,6 +9,12 @@ import jakarta.ws.rs.Path; @Path("/realms/{realm-key}") public class RealmResource { + @Path("/") + public RealmSubResource realms() + { + return CDI.current().select(RealmSubResource.class).get(); + } + @Path("/accounts") public UserResource accounts() { diff --git a/src/main/java/de/tavolio/realm/RealmSubResource.java b/src/main/java/de/tavolio/realm/RealmSubResource.java new file mode 100644 index 0000000..96c93ec --- /dev/null +++ b/src/main/java/de/tavolio/realm/RealmSubResource.java @@ -0,0 +1,25 @@ +package de.tavolio.realm; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.PathParam; + +@RequestScoped +public class RealmSubResource +{ + @Inject + RealmRepo repo; + + @GET + public Realm get(@PathParam("realm-key") String key) + { + RealmEntity entity = repo.findById(key); + if (entity != null) + { + return new Realm(entity.getKey(), entity.getName()); + } + throw new NotFoundException(); + } +} diff --git a/src/main/java/de/tavolio/realm/client/ClientEntity.java b/src/main/java/de/tavolio/realm/client/ClientEntity.java index ac85d8a..140c48c 100644 --- a/src/main/java/de/tavolio/realm/client/ClientEntity.java +++ b/src/main/java/de/tavolio/realm/client/ClientEntity.java @@ -3,9 +3,13 @@ package de.tavolio.realm.client; import de.tavolio.realm.RealmEntity; import de.tavolio.realm.RealmScoped; import de.tavolio.realm.code.CodeEntity; +import de.tavolio.realm.user.Permission; import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Entity @Table(name = "client") @@ -26,6 +30,14 @@ public class ClientEntity implements RealmScoped @OneToMany(mappedBy = "client") private List codes; + @ElementCollection + @CollectionTable( + name = "client_permission", + joinColumns = @JoinColumn(name = "client_id") + ) + @Column(name = "permission") + private Set permissions = new HashSet<>(); + public String getId() { return id; @@ -80,4 +92,15 @@ public class ClientEntity implements RealmScoped this.codes = codes; return this; } + + public Set getPermissions() + { + return permissions; + } + + public ClientEntity setPermissions(Set permissions) + { + this.permissions = permissions; + return this; + } } diff --git a/src/main/java/de/tavolio/realm/client/ClientService.java b/src/main/java/de/tavolio/realm/client/ClientService.java index 70218ce..d352693 100644 --- a/src/main/java/de/tavolio/realm/client/ClientService.java +++ b/src/main/java/de/tavolio/realm/client/ClientService.java @@ -1,5 +1,6 @@ package de.tavolio.realm.client; +import de.tavolio.bootstrap.Credentials; import de.tavolio.bootstrap.model.Client; import de.tavolio.realm.RealmEntity; import de.tavolio.realm.RealmRepo; @@ -21,16 +22,6 @@ public class ClientService @Inject RealmRepo realmRepo; - public ClientEntity findOrCreate(RealmEntity realm, Map.Entry 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); diff --git a/src/main/java/de/tavolio/realm/key/KeypairEntity.java b/src/main/java/de/tavolio/realm/key/KeypairEntity.java index 7048906..d80e06d 100644 --- a/src/main/java/de/tavolio/realm/key/KeypairEntity.java +++ b/src/main/java/de/tavolio/realm/key/KeypairEntity.java @@ -2,8 +2,15 @@ package de.tavolio.realm.key; import de.tavolio.realm.RealmEntity; import de.tavolio.realm.RealmScoped; +import de.tavolio.realm.RealmService; import jakarta.persistence.*; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; + @Entity @Table(name = "keypair") public class KeypairEntity implements RealmScoped @@ -128,4 +135,17 @@ public class KeypairEntity implements RealmScoped this.y = y; return this; } + + public static PrivateKey toPrivateKey(KeypairEntity keypair) + { + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keypair.getPrivateKey()); + try + { + return KeyFactory.getInstance(RealmService.EC).generatePrivate(spec); + } + catch (InvalidKeySpecException | NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/de/tavolio/realm/key/KeypairService.java b/src/main/java/de/tavolio/realm/key/KeypairService.java index bd1405d..2f7e59b 100644 --- a/src/main/java/de/tavolio/realm/key/KeypairService.java +++ b/src/main/java/de/tavolio/realm/key/KeypairService.java @@ -51,8 +51,8 @@ public class KeypairService .setUse("sig") .setAlg("ES256") .setCrv("P-256") - .setX(Base64.getUrlEncoder().withoutPadding().encodeToString(xBytes)) - .setY(Base64.getUrlEncoder().withoutPadding().encodeToString(yBytes)); + .setX(Base64.getEncoder().withoutPadding().encodeToString(xBytes)) + .setY(Base64.getEncoder().withoutPadding().encodeToString(yBytes)); } private KeyPair generate() diff --git a/src/main/java/de/tavolio/realm/user/Permission.java b/src/main/java/de/tavolio/realm/user/Permission.java new file mode 100644 index 0000000..138ab93 --- /dev/null +++ b/src/main/java/de/tavolio/realm/user/Permission.java @@ -0,0 +1,6 @@ +package de.tavolio.realm.user; + +public enum Permission +{ + USER_VIEW, USER_CREATE +} diff --git a/src/main/java/de/tavolio/realm/user/UserResource.java b/src/main/java/de/tavolio/realm/user/UserResource.java index 30867bd..2d87e79 100644 --- a/src/main/java/de/tavolio/realm/user/UserResource.java +++ b/src/main/java/de/tavolio/realm/user/UserResource.java @@ -1,5 +1,6 @@ package de.tavolio.realm.user; +import de.tavolio.realm.PermissionService; import de.tavolio.realm.user.dto.User; import de.tavolio.realm.user.dto.UserCreation; import jakarta.enterprise.context.RequestScoped; @@ -20,27 +21,34 @@ public class UserResource @Inject Logger LOG; + @PathParam("realm-key") + String realmKey; + @Inject UserService userService; + @Inject + PermissionService permissionService; + @POST public User post(@Valid UserCreation account) { - User createdUser = userService.create("", account); - LOG.infof("Created account successfully: %s", account.email()); - return createdUser; + permissionService.hasPermission(Permission.USER_CREATE); + return userService.create(realmKey, account); } @GET - public User get(@PathParam("id") String id) + public List get() { - return userService.getUser(id); + permissionService.hasPermission(Permission.USER_VIEW); + return userService.get(); } @GET @Path("/{id}") public User getById(@PathParam("id") String id) { + permissionService.hasPermission(Permission.USER_VIEW); return userService.getUser(id); } @@ -48,6 +56,7 @@ public class UserResource @Path("/search") public Map get(List ids) { + permissionService.hasPermission(Permission.USER_VIEW); return userService.findByIds(ids); } } diff --git a/src/main/java/de/tavolio/verify/JwksService.java b/src/main/java/de/tavolio/verify/JwksService.java index 0f130b5..c5fb511 100644 --- a/src/main/java/de/tavolio/verify/JwksService.java +++ b/src/main/java/de/tavolio/verify/JwksService.java @@ -7,6 +7,7 @@ import de.tavolio.verify.jwks.EcPublicKey; import de.tavolio.verify.jwks.JwksKey; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.apache.commons.lang3.NotImplementedException; import java.util.LinkedList; import java.util.List; @@ -18,44 +19,23 @@ public class JwksService @Inject KeypairRepo keypairRepo; - public Optional findByKid(String kid) + public JwksKey findByKid(KeypairEntity keypair) { - KeypairEntity keypair = keypairRepo.findById(kid); - if (keypair != null) + switch (keypair.getType()) { - switch (keypair.getType()) + case "EC" -> { - case "EC" -> - { - return Optional.of(constructPublicKey(keypair)); - } - case "RSA" -> - { - throw new IllegalArgumentException(); - } + return constructPublicKey(keypair); + } + case "RSA" -> + { + throw new NotImplementedException(); + } + default -> + { + throw new IllegalArgumentException(); } } - return Optional.empty(); - } - - public List findByRealm(RealmEntity realm) - { - List 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) diff --git a/src/main/java/de/tavolio/verify/TokenVerifier.java b/src/main/java/de/tavolio/verify/TokenVerifier.java deleted file mode 100644 index 1ea9751..0000000 --- a/src/main/java/de/tavolio/verify/TokenVerifier.java +++ /dev/null @@ -1,81 +0,0 @@ -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 -{ - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - @Inject - JwksService jwksService; - - @Override - public Class getRequestType() - { - return TokenAuthenticationRequest.class; - } - - @Override - @ActivateRequestContext - public Uni 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 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 getHeader(String raw) throws JsonProcessingException - { - return OBJECT_MAPPER.readValue(new String(Base64.getDecoder().decode(section(raw))), new TypeReference>(){}); - } - - private String section(String raw) - { - String[] sections = raw.split("\\."); - if (sections.length == 3) - { - return sections[0]; - } - throw new RuntimeException(); - } -} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 149839a..f98b894 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,15 +6,6 @@ quarkus.http.test-port=9089 quarkus.http.cors.enabled=true %dev.quarkus.http.cors.origins=/.*/ -# JWT -%prod.smallrye.jwt.sign.key.location=${PRIVATE_KEY_LOCATION} -%prod.mp.jwt.verify.publickey.location=${PUBLIC_KEY_LOCATION} - -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 prod.quarkus.hibernate-orm.validate-in-dev-mode=false quarkus.hibernate-orm.schema-management.strategy=none @@ -25,10 +16,6 @@ quarkus.datasource.db-kind=postgresql %dev,test.quarkus.datasource.password=postgres %dev,test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=auth -%prod.quarkus.datasource.username=${DB_USER} -%prod.quarkus.datasource.password=${DB_PASSWORD} -%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?currentSchema=${DB_SCHEMA} - # Flyway quarkus.flyway.enabled=false %test.quarkus.flyway.clean-at-start=true @@ -37,16 +24,10 @@ quarkus.flyway.enabled=false %test,dev.quarkus.flyway.migrate-at-start=false quarkus.flyway.migrate-at-start=true -# IAM Superuser -%test,dev.iam.user.name=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 +io.verifoo.http.origin=http://localhost:8089 -quarkus.log.level=DEBUG \ No newline at end of file +%dev.io.verifoo.superuser.username=admin +%dev.io.verifoo.superuser.password=pw \ No newline at end of file diff --git a/src/main/resources/bootstrap.yaml b/src/main/resources/bootstrap.yaml index d583b11..775a866 100644 --- a/src/main/resources/bootstrap.yaml +++ b/src/main/resources/bootstrap.yaml @@ -1,22 +1,22 @@ realms: maven: - name: My Bootstrap Realm + name: MavenVault audience: - strategy: realm + strategy: static + value: https://maven.dinauer.dev/api key: type: EC alg: P256 clients: backend: - client-secret: MY_SECRET_ENV - permissions: - - USER:VIEW - frontend: + secret: + bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX. redirect-uri: http://localhost:8080/callback - accounts: + permissions: + - USER_VIEW + users: - email: andreas.j.dinauer@gmail.com first-name: Andreas last-name: Dinauer - password-plain: pw - roles: - - USER:VIEW \ No newline at end of file + password: + bcrypt: $2a$12$vvHIfs6MWovIulGcuHeDie7yKtZzkBEPpNPiuj3C2HmUASYQ/x5SO \ No newline at end of file