♻️ Transform to IDP provider
This commit is contained in:
parent
79104dd02f
commit
7d638e6530
@ -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<Account> accounts)
|
|
||||||
{
|
|
||||||
for (Account account : accounts)
|
|
||||||
{
|
|
||||||
run(realm, account);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run(RealmEntity realm, Account account)
|
|
||||||
{
|
|
||||||
Optional<UserEntity> existingAccount = userRepo.findOptionalByRealmAndEmail(realm, account.email());
|
|
||||||
if (existingAccount.isEmpty())
|
|
||||||
{
|
|
||||||
UserEntity newAccount = new UserEntity();
|
|
||||||
newAccount.setId(UUID.randomUUID().toString());
|
|
||||||
newAccount.setEmail(account.email());
|
|
||||||
newAccount.setFirstname(account.firstname());
|
|
||||||
newAccount.setLastname(account.lastname());
|
|
||||||
newAccount.setRealm(realm);
|
|
||||||
newAccount.setStatus(UserStatus.INIT);
|
|
||||||
newAccount.setPassword(resolvePassword(account));
|
|
||||||
userRepo.persist(newAccount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolvePassword(Account account)
|
|
||||||
{
|
|
||||||
if (account.passwordFromEnv() != null)
|
|
||||||
{
|
|
||||||
return BcryptUtil.bcryptHash(System.getenv(account.passwordFromEnv()));
|
|
||||||
}
|
|
||||||
if (account.passwordPlain() != null)
|
|
||||||
{
|
|
||||||
return BcryptUtil.bcryptHash(account.passwordPlain());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,6 +9,7 @@ import jakarta.enterprise.context.ApplicationScoped;
|
|||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ClientBootstrapper
|
public class ClientBootstrapper
|
||||||
@ -23,8 +24,23 @@ public class ClientBootstrapper
|
|||||||
{
|
{
|
||||||
for (Map.Entry<String, Client> clientEntry : clients.entrySet())
|
for (Map.Entry<String, Client> clientEntry : clients.entrySet())
|
||||||
{
|
{
|
||||||
ClientEntity client = clientService.findOrCreate(realm, clientEntry);
|
run(realm, clientEntry);
|
||||||
clientRepo.persist(client);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run(RealmEntity realm, Map.Entry<String, Client> bootstrap)
|
||||||
|
{
|
||||||
|
Optional<ClientEntity> 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/main/java/de/tavolio/bootstrap/Credentials.java
Normal file
36
src/main/java/de/tavolio/bootstrap/Credentials.java
Normal file
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,14 +26,11 @@ public class RealmBootstrapService
|
|||||||
@Inject
|
@Inject
|
||||||
ClientBootstrapper clientBootstrapper;
|
ClientBootstrapper clientBootstrapper;
|
||||||
|
|
||||||
@Inject
|
|
||||||
RoleBootstrapper roleBootstrapper;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
RealmRepo realmRepo;
|
RealmRepo realmRepo;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AccountBootstrapper accountBootstrapper;
|
UserBootstrapper userBootstrapper;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AudienceStrategyRepo audienceStrategyRepo;
|
AudienceStrategyRepo audienceStrategyRepo;
|
||||||
@ -42,10 +39,9 @@ public class RealmBootstrapService
|
|||||||
{
|
{
|
||||||
Realm realmValue = realmEntry.getValue();
|
Realm realmValue = realmEntry.getValue();
|
||||||
RealmEntity realm = run(realmEntry.getKey(), realmEntry.getValue());
|
RealmEntity realm = run(realmEntry.getKey(), realmEntry.getValue());
|
||||||
roleBootstrapper.bootstrap(realm, realmValue.roles());
|
|
||||||
keyBootstrapper.bootstrap(realm, realmValue.key());
|
keyBootstrapper.bootstrap(realm, realmValue.key());
|
||||||
clientBootstrapper.bootstrap(realm, realmValue.clients());
|
clientBootstrapper.bootstrap(realm, realmValue.clients());
|
||||||
accountBootstrapper.bootstrap(realm, realmValue.accounts());
|
userBootstrapper.bootstrap(realm, realmValue.users());
|
||||||
}
|
}
|
||||||
|
|
||||||
public RealmEntity run(String key, Realm realm)
|
public RealmEntity run(String key, Realm realm)
|
||||||
|
|||||||
@ -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<String, Role> roles)
|
|
||||||
{
|
|
||||||
for (Map.Entry<String, Role> roleEntry : roles.entrySet())
|
|
||||||
{
|
|
||||||
run(realm, roleEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run(RealmEntity realm, Map.Entry<String, Role> roleEntry)
|
|
||||||
{
|
|
||||||
RoleEntity role = getOrCreateRole(realm, roleEntry.getKey());
|
|
||||||
for (String permission : roleEntry.getValue().permissions())
|
|
||||||
{
|
|
||||||
if (!role.hasPermission(permission))
|
|
||||||
{
|
|
||||||
role.getPermissions().add(permission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
roleRepo.persist(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RoleEntity getOrCreateRole(RealmEntity realm, String role)
|
|
||||||
{
|
|
||||||
return roleRepo.findByNameAndRealmOptional(role, realm).orElse(new RoleEntity().setId(UUID.randomUUID().toString()).setName(role).setRealm(realm));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -20,8 +20,8 @@ public class SuperuserBootstrapper
|
|||||||
public void bootstrap()
|
public void bootstrap()
|
||||||
{
|
{
|
||||||
Config config = ConfigProvider.getConfig();
|
Config config = ConfigProvider.getConfig();
|
||||||
String username = config.getValue("dev.dinauer.idp.superuser.username", String.class);
|
String username = config.getValue("io.verifoo.superuser.username", String.class);
|
||||||
String password = config.getValue("dev.dinauer.idp.superuser.password", String.class);
|
String password = config.getValue("io.verifoo.superuser.password", String.class);
|
||||||
if (!StringUtils.isBlank(username) && !StringUtils.isBlank(password) && superuserRepo.count() == 0)
|
if (!StringUtils.isBlank(username) && !StringUtils.isBlank(password) && superuserRepo.count() == 0)
|
||||||
{
|
{
|
||||||
SuperuserEntity superuser = new SuperuserEntity();
|
SuperuserEntity superuser = new SuperuserEntity();
|
||||||
|
|||||||
48
src/main/java/de/tavolio/bootstrap/UserBootstrapper.java
Normal file
48
src/main/java/de/tavolio/bootstrap/UserBootstrapper.java
Normal file
@ -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<User> users)
|
||||||
|
{
|
||||||
|
for (User user : users)
|
||||||
|
{
|
||||||
|
run(realm, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run(RealmEntity realm, User user)
|
||||||
|
{
|
||||||
|
Optional<UserEntity> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,9 +1,15 @@
|
|||||||
package de.tavolio.bootstrap.model;
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
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<String> roles)
|
public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uri") String redirectURI, @JsonProperty("permissions") Set<String> permissionList)
|
||||||
{
|
{
|
||||||
|
public Set<Permission> permissions()
|
||||||
|
{
|
||||||
|
return permissionList.stream().map(item -> Permission.valueOf(item.toUpperCase())).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
5
src/main/java/de/tavolio/bootstrap/model/Credential.java
Normal file
5
src/main/java/de/tavolio/bootstrap/model/Credential.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.tavolio.bootstrap.model;
|
||||||
|
|
||||||
|
public record Credential(String plain, String bcrypt, String env)
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -3,6 +3,6 @@ package de.tavolio.bootstrap.model;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public record Realm(String name, Key key, Audience audience, Map<String, Client> clients, Map<String, Role> roles, List<String> permissions, List<Account> accounts)
|
public record Realm(String name, Key key, Audience audience, Map<String, Client> clients, Map<String, Role> roles, List<String> permissions, List<User> users)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/main/java/de/tavolio/bootstrap/model/User.java
Normal file
7
src/main/java/de/tavolio/bootstrap/model/User.java
Normal file
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class IssuerService
|
public class IssuerService
|
||||||
{
|
{
|
||||||
@ConfigProperty(name = "dev.dinauer.idp.origin")
|
@ConfigProperty(name = "io.verifoo.http.origin")
|
||||||
String origin;
|
String origin;
|
||||||
|
|
||||||
public String getIssuer(String realmKey)
|
public String getIssuer(String realmKey)
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public class OidcConfigurationResource
|
|||||||
@Inject
|
@Inject
|
||||||
IssuerService issuerService;
|
IssuerService issuerService;
|
||||||
|
|
||||||
@ConfigProperty(name = "dev.dinauer.idp.origin")
|
@ConfigProperty(name = "io.verifoo.http.origin")
|
||||||
String origin;
|
String origin;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
package de.tavolio.oidc;
|
package de.tavolio.oidc;
|
||||||
|
|
||||||
import de.tavolio.oidc.session.AuthorizationService;
|
import de.tavolio.oidc.auth.AuthorizationService;
|
||||||
import de.tavolio.oidc.session.dto.SessionCreation;
|
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.model.TokenResponse;
|
||||||
import de.tavolio.oidc.token.TokenService;
|
import de.tavolio.oidc.token.UserTokenService;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.enterprise.context.RequestScoped;
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
@ -26,11 +27,14 @@ public class OidcResource
|
|||||||
JwksService jwksService;
|
JwksService jwksService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
TokenService tokenService;
|
UserTokenService userTokenService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AuthorizationService authorizationService;
|
AuthorizationService authorizationService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ClientTokenService clientTokenService;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/certs")
|
@Path("/certs")
|
||||||
public Map<String, Object> certs()
|
public Map<String, Object> certs()
|
||||||
@ -42,7 +46,7 @@ public class OidcResource
|
|||||||
@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, @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();
|
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")
|
@Path("/token")
|
||||||
@RolesAllowed("CLIENT")
|
@RolesAllowed("CLIENT")
|
||||||
@Transactional
|
@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)))
|
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)))
|
if (GrantType.CLIENT_CREDENTIALS.equals(GrantType.fromValue(grantType)))
|
||||||
{
|
{
|
||||||
return tokenService.getClientToken(realmKey);
|
return clientTokenService.getToken(realmKey);
|
||||||
}
|
}
|
||||||
throw new RuntimeException();
|
throw new RuntimeException();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package de.tavolio.oidc.session;
|
package de.tavolio.oidc.auth;
|
||||||
|
|
||||||
import de.tavolio.realm.RealmService;
|
import de.tavolio.realm.RealmService;
|
||||||
import de.tavolio.realm.user.UserEntity;
|
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.CodeEntity;
|
||||||
import de.tavolio.realm.code.CodeRepo;
|
import de.tavolio.realm.code.CodeRepo;
|
||||||
import de.tavolio.realm.RealmEntity;
|
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 io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
@ -35,16 +35,16 @@ public class AuthorizationService
|
|||||||
ClientService clientService;
|
ClientService clientService;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public String generateBySessionCreation(String realmKey, String clientId, SessionCreation sessionCreation)
|
public String generateBySessionCreation(String realmKey, String clientId, AuthorizationCreation authorizationCreation)
|
||||||
{
|
{
|
||||||
|
|
||||||
RealmEntity realm = realmService.requireByKey(realmKey);
|
RealmEntity realm = realmService.requireByKey(realmKey);
|
||||||
ClientEntity client = clientService.findByIdAndRealm(clientId, realm);
|
ClientEntity client = clientService.findByIdAndRealm(clientId, realm);
|
||||||
Optional<UserEntity> accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, sessionCreation.email());
|
Optional<UserEntity> accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, authorizationCreation.email());
|
||||||
if (accountEntityOptional.isPresent())
|
if (accountEntityOptional.isPresent())
|
||||||
{
|
{
|
||||||
UserEntity userEntity = accountEntityOptional.get();
|
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());
|
CodeEntity code = new CodeEntity().setId(UUID.randomUUID().toString());
|
||||||
code.setExpiresAt(ZonedDateTime.now().plusMinutes(1));
|
code.setExpiresAt(ZonedDateTime.now().plusMinutes(1));
|
||||||
@ -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.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
public record SessionCreation(
|
public record AuthorizationCreation(
|
||||||
@Email String email,
|
@Email String email,
|
||||||
@NotBlank String password)
|
@NotBlank String password)
|
||||||
{
|
{
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package de.tavolio.oidc.auth;
|
package de.tavolio.oidc.identityproviders;
|
||||||
|
|
||||||
import de.tavolio.Role;
|
import de.tavolio.Role;
|
||||||
import de.tavolio.realm.user.UserRepo;
|
import de.tavolio.realm.user.UserRepo;
|
||||||
@ -16,20 +16,20 @@ import io.quarkus.security.runtime.QuarkusPrincipal;
|
|||||||
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
|
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
import io.smallrye.mutiny.infrastructure.Infrastructure;
|
import io.smallrye.mutiny.infrastructure.Infrastructure;
|
||||||
|
import jakarta.annotation.Priority;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.enterprise.context.control.ActivateRequestContext;
|
import jakarta.enterprise.context.control.ActivateRequestContext;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class OidcClientIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest>
|
@Priority(1)
|
||||||
|
public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest>
|
||||||
{
|
{
|
||||||
@Inject
|
@Inject
|
||||||
ClientRepo clientRepo;
|
ClientRepo clientRepo;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SuperuserRepo superuserRepo;
|
SuperuserRepo superuserRepo;
|
||||||
@Inject
|
|
||||||
UserRepo userRepo;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<UsernamePasswordAuthenticationRequest> getRequestType()
|
public Class<UsernamePasswordAuthenticationRequest> getRequestType()
|
||||||
@ -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<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.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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/main/java/de/tavolio/oidc/token/ClientTokenService.java
Normal file
48
src/main/java/de/tavolio/oidc/token/ClientTokenService.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ import java.security.PrivateKey;
|
|||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class TokenGenerator
|
public class UserTokenGenerator
|
||||||
{
|
{
|
||||||
@Inject
|
@Inject
|
||||||
IssuerService issuerService;
|
IssuerService issuerService;
|
||||||
@ -2,6 +2,8 @@ package de.tavolio.oidc.token;
|
|||||||
|
|
||||||
import de.tavolio.oidc.token.model.TokenResponse;
|
import de.tavolio.oidc.token.model.TokenResponse;
|
||||||
import de.tavolio.realm.RealmRepo;
|
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.CodeEntity;
|
||||||
import de.tavolio.realm.code.CodeRepo;
|
import de.tavolio.realm.code.CodeRepo;
|
||||||
import de.tavolio.realm.RealmEntity;
|
import de.tavolio.realm.RealmEntity;
|
||||||
@ -11,41 +13,31 @@ 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 jakarta.ws.rs.core.Context;
|
|
||||||
import org.apache.commons.lang3.NotImplementedException;
|
|
||||||
|
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class TokenService
|
public class UserTokenService
|
||||||
{
|
{
|
||||||
@Inject
|
@Inject
|
||||||
CodeRepo codeRepo;
|
CodeRepo codeRepo;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
TokenGenerator tokenGenerator;
|
UserTokenGenerator userTokenGenerator;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity identity;
|
SecurityIdentity identity;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
RealmRepo realmRepo;
|
RealmService realmService;
|
||||||
|
|
||||||
@POST
|
public TokenResponse getToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException
|
||||||
public TokenResponse getClientToken(String realmKey)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TokenResponse getUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException
|
|
||||||
{
|
{
|
||||||
return generateUserToken(realmKey, code);
|
return generateUserToken(realmKey, code);
|
||||||
}
|
}
|
||||||
@ -53,18 +45,17 @@ public class TokenService
|
|||||||
private TokenResponse generateUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException
|
private TokenResponse generateUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException
|
||||||
{
|
{
|
||||||
String principal = identity.getPrincipal().getName();
|
String principal = identity.getPrincipal().getName();
|
||||||
RealmEntity realm = realmRepo.findById(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().getId()))
|
if (entity != null && !ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getId()))
|
||||||
{
|
{
|
||||||
KeypairEntity keypair = realm.getKeys().getFirst();
|
KeypairEntity keypair = realm.getKeys().getFirst();
|
||||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keypair.getPrivateKey());
|
PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair);
|
||||||
ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1);
|
ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1);
|
||||||
PrivateKey signingKey = KeyFactory.getInstance(RealmService.EC).generatePrivate(spec);
|
|
||||||
TokenResponse response = new TokenResponse()
|
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())
|
.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")
|
.setTokenType("Bearer")
|
||||||
.setExpiresAt(expiresAt.toInstant().getEpochSecond());
|
.setExpiresAt(expiresAt.toInstant().getEpochSecond());
|
||||||
codeRepo.delete(entity);
|
codeRepo.delete(entity);
|
||||||
34
src/main/java/de/tavolio/realm/PermissionService.java
Normal file
34
src/main/java/de/tavolio/realm/PermissionService.java
Normal file
@ -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<Permission> granted = identity.getAttribute("permissions");
|
||||||
|
for (Permission permission : required)
|
||||||
|
{
|
||||||
|
if (granted != null && granted.contains(permission))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/main/java/de/tavolio/realm/Realm.java
Normal file
5
src/main/java/de/tavolio/realm/Realm.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.tavolio.realm;
|
||||||
|
|
||||||
|
public record Realm(String key, String name)
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -9,6 +9,12 @@ import jakarta.ws.rs.Path;
|
|||||||
@Path("/realms/{realm-key}")
|
@Path("/realms/{realm-key}")
|
||||||
public class RealmResource
|
public class RealmResource
|
||||||
{
|
{
|
||||||
|
@Path("/")
|
||||||
|
public RealmSubResource realms()
|
||||||
|
{
|
||||||
|
return CDI.current().select(RealmSubResource.class).get();
|
||||||
|
}
|
||||||
|
|
||||||
@Path("/accounts")
|
@Path("/accounts")
|
||||||
public UserResource accounts()
|
public UserResource accounts()
|
||||||
{
|
{
|
||||||
|
|||||||
25
src/main/java/de/tavolio/realm/RealmSubResource.java
Normal file
25
src/main/java/de/tavolio/realm/RealmSubResource.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,9 +3,13 @@ package de.tavolio.realm.client;
|
|||||||
import de.tavolio.realm.RealmEntity;
|
import de.tavolio.realm.RealmEntity;
|
||||||
import de.tavolio.realm.RealmScoped;
|
import de.tavolio.realm.RealmScoped;
|
||||||
import de.tavolio.realm.code.CodeEntity;
|
import de.tavolio.realm.code.CodeEntity;
|
||||||
|
import de.tavolio.realm.user.Permission;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "client")
|
@Table(name = "client")
|
||||||
@ -26,6 +30,14 @@ public class ClientEntity implements RealmScoped
|
|||||||
@OneToMany(mappedBy = "client")
|
@OneToMany(mappedBy = "client")
|
||||||
private List<CodeEntity> codes;
|
private List<CodeEntity> codes;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(
|
||||||
|
name = "client_permission",
|
||||||
|
joinColumns = @JoinColumn(name = "client_id")
|
||||||
|
)
|
||||||
|
@Column(name = "permission")
|
||||||
|
private Set<Permission> permissions = new HashSet<>();
|
||||||
|
|
||||||
public String getId()
|
public String getId()
|
||||||
{
|
{
|
||||||
return id;
|
return id;
|
||||||
@ -80,4 +92,15 @@ public class ClientEntity implements RealmScoped
|
|||||||
this.codes = codes;
|
this.codes = codes;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<Permission> getPermissions()
|
||||||
|
{
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientEntity setPermissions(Set<Permission> permissions)
|
||||||
|
{
|
||||||
|
this.permissions = permissions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.tavolio.realm.client;
|
package de.tavolio.realm.client;
|
||||||
|
|
||||||
|
import de.tavolio.bootstrap.Credentials;
|
||||||
import de.tavolio.bootstrap.model.Client;
|
import de.tavolio.bootstrap.model.Client;
|
||||||
import de.tavolio.realm.RealmEntity;
|
import de.tavolio.realm.RealmEntity;
|
||||||
import de.tavolio.realm.RealmRepo;
|
import de.tavolio.realm.RealmRepo;
|
||||||
@ -21,16 +22,6 @@ public class ClientService
|
|||||||
@Inject
|
@Inject
|
||||||
RealmRepo realmRepo;
|
RealmRepo realmRepo;
|
||||||
|
|
||||||
public ClientEntity findOrCreate(RealmEntity realm, Map.Entry<String, Client> bootstrap)
|
|
||||||
{
|
|
||||||
String secret = null;
|
|
||||||
if (!StringUtils.isBlank(bootstrap.getValue().clientSecret()))
|
|
||||||
{
|
|
||||||
secret = BcryptUtil.bcryptHash(System.getenv(bootstrap.getValue().clientSecret()));
|
|
||||||
}
|
|
||||||
return clientRepo.findByRealmAndIdOptional(realm, bootstrap.getKey()).orElse(new ClientEntity().setId(bootstrap.getKey()).setSecret(secret).setRedirectURI(bootstrap.getValue().redirectURI()).setRealm(realm));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientEntity findByIdAndRealm(String clientId, RealmEntity realm)
|
public ClientEntity findByIdAndRealm(String clientId, RealmEntity realm)
|
||||||
{
|
{
|
||||||
ClientEntity client = clientRepo.findByRealmAndId(realm, clientId);
|
ClientEntity client = clientRepo.findByRealmAndId(realm, clientId);
|
||||||
|
|||||||
@ -2,8 +2,15 @@ package de.tavolio.realm.key;
|
|||||||
|
|
||||||
import de.tavolio.realm.RealmEntity;
|
import de.tavolio.realm.RealmEntity;
|
||||||
import de.tavolio.realm.RealmScoped;
|
import de.tavolio.realm.RealmScoped;
|
||||||
|
import de.tavolio.realm.RealmService;
|
||||||
import jakarta.persistence.*;
|
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
|
@Entity
|
||||||
@Table(name = "keypair")
|
@Table(name = "keypair")
|
||||||
public class KeypairEntity implements RealmScoped
|
public class KeypairEntity implements RealmScoped
|
||||||
@ -128,4 +135,17 @@ public class KeypairEntity implements RealmScoped
|
|||||||
this.y = y;
|
this.y = y;
|
||||||
return this;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,8 +51,8 @@ public class KeypairService
|
|||||||
.setUse("sig")
|
.setUse("sig")
|
||||||
.setAlg("ES256")
|
.setAlg("ES256")
|
||||||
.setCrv("P-256")
|
.setCrv("P-256")
|
||||||
.setX(Base64.getUrlEncoder().withoutPadding().encodeToString(xBytes))
|
.setX(Base64.getEncoder().withoutPadding().encodeToString(xBytes))
|
||||||
.setY(Base64.getUrlEncoder().withoutPadding().encodeToString(yBytes));
|
.setY(Base64.getEncoder().withoutPadding().encodeToString(yBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeyPair generate()
|
private KeyPair generate()
|
||||||
|
|||||||
6
src/main/java/de/tavolio/realm/user/Permission.java
Normal file
6
src/main/java/de/tavolio/realm/user/Permission.java
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package de.tavolio.realm.user;
|
||||||
|
|
||||||
|
public enum Permission
|
||||||
|
{
|
||||||
|
USER_VIEW, USER_CREATE
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package de.tavolio.realm.user;
|
package de.tavolio.realm.user;
|
||||||
|
|
||||||
|
import de.tavolio.realm.PermissionService;
|
||||||
import de.tavolio.realm.user.dto.User;
|
import de.tavolio.realm.user.dto.User;
|
||||||
import de.tavolio.realm.user.dto.UserCreation;
|
import de.tavolio.realm.user.dto.UserCreation;
|
||||||
import jakarta.enterprise.context.RequestScoped;
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
@ -20,27 +21,34 @@ public class UserResource
|
|||||||
@Inject
|
@Inject
|
||||||
Logger LOG;
|
Logger LOG;
|
||||||
|
|
||||||
|
@PathParam("realm-key")
|
||||||
|
String realmKey;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
UserService userService;
|
UserService userService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
PermissionService permissionService;
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
public User post(@Valid UserCreation account)
|
public User post(@Valid UserCreation account)
|
||||||
{
|
{
|
||||||
User createdUser = userService.create("", account);
|
permissionService.hasPermission(Permission.USER_CREATE);
|
||||||
LOG.infof("Created account successfully: %s", account.email());
|
return userService.create(realmKey, account);
|
||||||
return createdUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
public User get(@PathParam("id") String id)
|
public List<User> get()
|
||||||
{
|
{
|
||||||
return userService.getUser(id);
|
permissionService.hasPermission(Permission.USER_VIEW);
|
||||||
|
return userService.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
public User getById(@PathParam("id") String id)
|
public User getById(@PathParam("id") String id)
|
||||||
{
|
{
|
||||||
|
permissionService.hasPermission(Permission.USER_VIEW);
|
||||||
return userService.getUser(id);
|
return userService.getUser(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +56,7 @@ public class UserResource
|
|||||||
@Path("/search")
|
@Path("/search")
|
||||||
public Map<String, User> get(List<String> ids)
|
public Map<String, User> get(List<String> ids)
|
||||||
{
|
{
|
||||||
|
permissionService.hasPermission(Permission.USER_VIEW);
|
||||||
return userService.findByIds(ids);
|
return userService.findByIds(ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import de.tavolio.verify.jwks.EcPublicKey;
|
|||||||
import de.tavolio.verify.jwks.JwksKey;
|
import de.tavolio.verify.jwks.JwksKey;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -18,44 +19,23 @@ public class JwksService
|
|||||||
@Inject
|
@Inject
|
||||||
KeypairRepo keypairRepo;
|
KeypairRepo keypairRepo;
|
||||||
|
|
||||||
public Optional<JwksKey> findByKid(String kid)
|
public JwksKey findByKid(KeypairEntity keypair)
|
||||||
{
|
{
|
||||||
KeypairEntity keypair = keypairRepo.findById(kid);
|
switch (keypair.getType())
|
||||||
if (keypair != null)
|
|
||||||
{
|
{
|
||||||
switch (keypair.getType())
|
case "EC" ->
|
||||||
{
|
{
|
||||||
case "EC" ->
|
return constructPublicKey(keypair);
|
||||||
{
|
}
|
||||||
return Optional.of(constructPublicKey(keypair));
|
case "RSA" ->
|
||||||
}
|
{
|
||||||
case "RSA" ->
|
throw new NotImplementedException();
|
||||||
{
|
}
|
||||||
throw new IllegalArgumentException();
|
default ->
|
||||||
}
|
{
|
||||||
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<JwksKey> findByRealm(RealmEntity realm)
|
|
||||||
{
|
|
||||||
List<JwksKey> result = new LinkedList<>();
|
|
||||||
for (KeypairEntity keypair : realm.getKeys())
|
|
||||||
{
|
|
||||||
switch (keypair.getType())
|
|
||||||
{
|
|
||||||
case "EC" ->
|
|
||||||
{
|
|
||||||
result.add(constructPublicKey(keypair));
|
|
||||||
}
|
|
||||||
case "RSA" ->
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private EcPublicKey constructPublicKey(KeypairEntity entity)
|
private EcPublicKey constructPublicKey(KeypairEntity entity)
|
||||||
|
|||||||
@ -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<TokenAuthenticationRequest>
|
|
||||||
{
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
JwksService jwksService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Class<TokenAuthenticationRequest> getRequestType()
|
|
||||||
{
|
|
||||||
return TokenAuthenticationRequest.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@ActivateRequestContext
|
|
||||||
public Uni<SecurityIdentity> authenticate(TokenAuthenticationRequest tokenAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext)
|
|
||||||
{
|
|
||||||
String raw = tokenAuthenticationRequest.getToken().getToken();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Object kid = getHeader(raw).get("kid");
|
|
||||||
if (kid instanceof String keyId)
|
|
||||||
{
|
|
||||||
return Uni.createFrom().item(() -> {
|
|
||||||
Optional<JwksKey> keypair = jwksService.findByKid(keyId);
|
|
||||||
if (keypair.isPresent())
|
|
||||||
{
|
|
||||||
PublicKey publicKey = keypair.get().toPublicKey();
|
|
||||||
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal("")).build();
|
|
||||||
}
|
|
||||||
throw new AuthenticationFailedException();
|
|
||||||
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool());
|
|
||||||
}
|
|
||||||
return Uni.createFrom().nullItem();
|
|
||||||
}
|
|
||||||
catch (JsonProcessingException e)
|
|
||||||
{
|
|
||||||
return Uni.createFrom().failure(new AuthenticationFailedException());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> getHeader(String raw) throws JsonProcessingException
|
|
||||||
{
|
|
||||||
return OBJECT_MAPPER.readValue(new String(Base64.getDecoder().decode(section(raw))), new TypeReference<Map<String, Object>>(){});
|
|
||||||
}
|
|
||||||
|
|
||||||
private String section(String raw)
|
|
||||||
{
|
|
||||||
String[] sections = raw.split("\\.");
|
|
||||||
if (sections.length == 3)
|
|
||||||
{
|
|
||||||
return sections[0];
|
|
||||||
}
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,15 +6,6 @@ quarkus.http.test-port=9089
|
|||||||
quarkus.http.cors.enabled=true
|
quarkus.http.cors.enabled=true
|
||||||
%dev.quarkus.http.cors.origins=/.*/
|
%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
|
# 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
|
||||||
@ -25,10 +16,6 @@ quarkus.datasource.db-kind=postgresql
|
|||||||
%dev,test.quarkus.datasource.password=postgres
|
%dev,test.quarkus.datasource.password=postgres
|
||||||
%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
|
||||||
|
|
||||||
%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
|
# Flyway
|
||||||
quarkus.flyway.enabled=false
|
quarkus.flyway.enabled=false
|
||||||
%test.quarkus.flyway.clean-at-start=true
|
%test.quarkus.flyway.clean-at-start=true
|
||||||
@ -37,16 +24,10 @@ quarkus.flyway.enabled=false
|
|||||||
%test,dev.quarkus.flyway.migrate-at-start=false
|
%test,dev.quarkus.flyway.migrate-at-start=false
|
||||||
quarkus.flyway.migrate-at-start=true
|
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.access-log.enabled=true
|
||||||
quarkus.http.auth.basic=true
|
quarkus.http.auth.basic=true
|
||||||
dev.dinauer.idp.origin=http://localhost:8089
|
|
||||||
|
|
||||||
%dev.dev.dinauer.idp.superuser.username=admin
|
io.verifoo.http.origin=http://localhost:8089
|
||||||
%dev.dev.dinauer.idp.superuser.password=pw
|
|
||||||
|
|
||||||
quarkus.log.level=DEBUG
|
%dev.io.verifoo.superuser.username=admin
|
||||||
|
%dev.io.verifoo.superuser.password=pw
|
||||||
@ -1,22 +1,22 @@
|
|||||||
realms:
|
realms:
|
||||||
maven:
|
maven:
|
||||||
name: My Bootstrap Realm
|
name: MavenVault
|
||||||
audience:
|
audience:
|
||||||
strategy: realm
|
strategy: static
|
||||||
|
value: https://maven.dinauer.dev/api
|
||||||
key:
|
key:
|
||||||
type: EC
|
type: EC
|
||||||
alg: P256
|
alg: P256
|
||||||
clients:
|
clients:
|
||||||
backend:
|
backend:
|
||||||
client-secret: MY_SECRET_ENV
|
secret:
|
||||||
permissions:
|
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
|
||||||
- USER:VIEW
|
|
||||||
frontend:
|
|
||||||
redirect-uri: http://localhost:8080/callback
|
redirect-uri: http://localhost:8080/callback
|
||||||
accounts:
|
permissions:
|
||||||
|
- USER_VIEW
|
||||||
|
users:
|
||||||
- email: andreas.j.dinauer@gmail.com
|
- email: andreas.j.dinauer@gmail.com
|
||||||
first-name: Andreas
|
first-name: Andreas
|
||||||
last-name: Dinauer
|
last-name: Dinauer
|
||||||
password-plain: pw
|
password:
|
||||||
roles:
|
bcrypt: $2a$12$vvHIfs6MWovIulGcuHeDie7yKtZzkBEPpNPiuj3C2HmUASYQ/x5SO
|
||||||
- USER:VIEW
|
|
||||||
Loading…
x
Reference in New Issue
Block a user