From 25a3702d8c86bd73699cf786cf419fa565189d4c Mon Sep 17 00:00:00 2001 From: Daniel Fiala Date: Sun, 18 Aug 2024 17:32:02 +0200 Subject: [PATCH] feat(access-tokens): added endpoint and separate table for external access tokens --- .../response/ExternalAccessTokenResponse.java | 47 +++++++++++++++++++ .../ExternalAccessTokensResponse.java | 18 +++++++ .../model/response/PrivateUsersResponse.java | 14 ++---- .../web/resource/AdminUsersResource.java | 19 ++++++-- .../feature/ListExternalAccessTokens.java | 9 ++++ .../impl/ListExternalAccessTokensImpl.java | 23 +++++++++ .../LoginUserWithAuthorizationCodeImpl.java | 21 +++++---- .../mapping/ExternalAccessTokenMapper.java | 19 ++++++++ .../zenei/domain/model/account/Account.java | 9 +--- .../model/account/ExternalAccessToken.java | 19 ++++++++ .../ExternalAccessTokenRepository.java | 8 ++++ ...ry.java => ExternalAccountRepository.java} | 2 +- .../entity/ExternalAccessTokenEntity.java | 38 ++++++++++++++- .../entity/ExternalAccountEntity.java | 20 ++------ .../ExternalAccessTokenRepositoryPanache.java | 27 +++++++++++ ... => ExternalAccountRepositoryPanache.java} | 5 +- .../panache/UserRepositoryPanache.java | 3 ++ .../resources/application-test.properties | 8 +--- .../AuthenticationFlowWithAdminUserTest.java | 6 +-- ...nticationFlowWithExternalProviderTest.java | 35 +++++++++++++- .../zenei/resource/TestProviderFeature.java | 4 +- .../external/ExternalAuthProvider.java | 4 +- .../DiscordExternalAuthProvider.java | 4 +- .../providers/GithubExternalAuthProvider.java | 4 +- .../providers/GoogleExternalAuthProvider.java | 4 +- .../web/client/ExternalAccessToken.java | 27 ----------- .../external/web/client/LoginOAuthClient.java | 2 +- 27 files changed, 300 insertions(+), 99 deletions(-) create mode 100644 core/src/main/java/dev/cloudeko/zenei/application/web/model/response/ExternalAccessTokenResponse.java create mode 100644 core/src/main/java/dev/cloudeko/zenei/application/web/model/response/ExternalAccessTokensResponse.java create mode 100644 core/src/main/java/dev/cloudeko/zenei/domain/feature/ListExternalAccessTokens.java create mode 100644 core/src/main/java/dev/cloudeko/zenei/domain/feature/impl/ListExternalAccessTokensImpl.java create mode 100644 core/src/main/java/dev/cloudeko/zenei/domain/mapping/ExternalAccessTokenMapper.java create mode 100644 core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccessToken.java create mode 100644 core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccessTokenRepository.java rename core/src/main/java/dev/cloudeko/zenei/domain/model/account/{AccountRepository.java => ExternalAccountRepository.java} (75%) create mode 100644 core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ExternalAccessTokenRepositoryPanache.java rename core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/{AccountRepositoryPanache.java => ExternalAccountRepositoryPanache.java} (81%) delete mode 100644 extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/web/client/ExternalAccessToken.java diff --git a/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/ExternalAccessTokenResponse.java b/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/ExternalAccessTokenResponse.java new file mode 100644 index 0000000..4d1416b --- /dev/null +++ b/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/ExternalAccessTokenResponse.java @@ -0,0 +1,47 @@ +package dev.cloudeko.zenei.application.web.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.cloudeko.zenei.domain.model.account.ExternalAccessToken; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Data +@NoArgsConstructor +@RegisterForReflection +@Schema(name = "External Access Token", description = "Represents an access token from an external provider") +public class ExternalAccessTokenResponse { + + @JsonProperty("id_token") + @Schema(description = "ID token") + private String idToken; + + @JsonProperty("refresh_token") + @Schema(description = "Refresh token") + private String refreshToken; + + @JsonProperty("access_token") + @Schema(description = "Access token") + private String accessToken; + + @JsonProperty("token_type") + @Schema(description = "Token type") + private String tokenType; + + @JsonProperty("scope") + @Schema(description = "Scope") + private String scope; + + @JsonProperty("access_token_expires_at") + private Long accessTokenExpiresAt; + + public ExternalAccessTokenResponse(ExternalAccessToken externalAccessToken) { + this.idToken = externalAccessToken.getIdToken(); + this.refreshToken = externalAccessToken.getRefreshToken(); + this.accessToken = externalAccessToken.getAccessToken(); + this.tokenType = externalAccessToken.getTokenType(); + this.scope = externalAccessToken.getScope(); + this.accessTokenExpiresAt = externalAccessToken.getAccessTokenExpiresAt(); + } +} diff --git a/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/ExternalAccessTokensResponse.java b/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/ExternalAccessTokensResponse.java new file mode 100644 index 0000000..8c48553 --- /dev/null +++ b/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/ExternalAccessTokensResponse.java @@ -0,0 +1,18 @@ +package dev.cloudeko.zenei.application.web.model.response; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.ArrayList; +import java.util.List; + +@NoArgsConstructor +@RegisterForReflection +@Schema(name = "External Access Token List", description = "Represents a list of external access tokens") +public class ExternalAccessTokensResponse extends ArrayList { + + public ExternalAccessTokensResponse(List externalAccessTokens) { + super(externalAccessTokens); + } +} diff --git a/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/PrivateUsersResponse.java b/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/PrivateUsersResponse.java index 1ed6dbb..f62f1a2 100644 --- a/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/PrivateUsersResponse.java +++ b/core/src/main/java/dev/cloudeko/zenei/application/web/model/response/PrivateUsersResponse.java @@ -1,22 +1,18 @@ package dev.cloudeko.zenei.application.web.model.response; -import com.fasterxml.jackson.annotation.JsonProperty; import io.quarkus.runtime.annotations.RegisterForReflection; -import lombok.AllArgsConstructor; -import lombok.Data; import lombok.NoArgsConstructor; import org.eclipse.microprofile.openapi.annotations.media.Schema; +import java.util.ArrayList; import java.util.List; -@Data @NoArgsConstructor -@AllArgsConstructor @RegisterForReflection @Schema(name = "Users", description = "Represents a list of users") -public class PrivateUsersResponse { +public class PrivateUsersResponse extends ArrayList { - @JsonProperty("users") - @Schema(description = "A list of users") - private List users; + public PrivateUsersResponse(List users) { + super(users); + } } diff --git a/core/src/main/java/dev/cloudeko/zenei/application/web/resource/AdminUsersResource.java b/core/src/main/java/dev/cloudeko/zenei/application/web/resource/AdminUsersResource.java index ad72894..0e31afd 100644 --- a/core/src/main/java/dev/cloudeko/zenei/application/web/resource/AdminUsersResource.java +++ b/core/src/main/java/dev/cloudeko/zenei/application/web/resource/AdminUsersResource.java @@ -1,12 +1,12 @@ package dev.cloudeko.zenei.application.web.resource; +import dev.cloudeko.zenei.application.web.model.response.ExternalAccessTokenResponse; +import dev.cloudeko.zenei.application.web.model.response.ExternalAccessTokensResponse; import dev.cloudeko.zenei.application.web.model.response.PrivateUserResponse; import dev.cloudeko.zenei.application.web.model.response.PrivateUsersResponse; +import dev.cloudeko.zenei.domain.feature.ListExternalAccessTokens; import dev.cloudeko.zenei.domain.feature.ListUsers; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.AllArgsConstructor; @@ -22,6 +22,7 @@ public class AdminUsersResource { private final ListUsers listUsers; + private final ListExternalAccessTokens listExternalAccessTokens; @GET @Produces(MediaType.APPLICATION_JSON) @@ -31,4 +32,14 @@ public Response getUsers(@QueryParam("page") Optional page, @QueryParam return Response.ok(new PrivateUsersResponse(usersResponse)).build(); } + + @GET + @Path("{userId}/external_access_tokens/{provider}") + @Produces(MediaType.APPLICATION_JSON) + public Response getExternalAccessToken(@PathParam("userId") Long userId, @PathParam("provider") String provider) { + final var externalAccessTokens = listExternalAccessTokens.listByProvider(userId, provider); + final var externalAccessTokensResponse = externalAccessTokens.stream().map(ExternalAccessTokenResponse::new).toList(); + + return Response.ok(new ExternalAccessTokensResponse(externalAccessTokensResponse)).build(); + } } diff --git a/core/src/main/java/dev/cloudeko/zenei/domain/feature/ListExternalAccessTokens.java b/core/src/main/java/dev/cloudeko/zenei/domain/feature/ListExternalAccessTokens.java new file mode 100644 index 0000000..f588623 --- /dev/null +++ b/core/src/main/java/dev/cloudeko/zenei/domain/feature/ListExternalAccessTokens.java @@ -0,0 +1,9 @@ +package dev.cloudeko.zenei.domain.feature; + +import dev.cloudeko.zenei.domain.model.account.ExternalAccessToken; + +import java.util.List; + +public interface ListExternalAccessTokens { + List listByProvider(long userId, String provider); +} diff --git a/core/src/main/java/dev/cloudeko/zenei/domain/feature/impl/ListExternalAccessTokensImpl.java b/core/src/main/java/dev/cloudeko/zenei/domain/feature/impl/ListExternalAccessTokensImpl.java new file mode 100644 index 0000000..9171e3f --- /dev/null +++ b/core/src/main/java/dev/cloudeko/zenei/domain/feature/impl/ListExternalAccessTokensImpl.java @@ -0,0 +1,23 @@ +package dev.cloudeko.zenei.domain.feature.impl; + +import dev.cloudeko.zenei.domain.feature.ListExternalAccessTokens; +import dev.cloudeko.zenei.domain.mapping.ExternalAccessTokenMapper; +import dev.cloudeko.zenei.domain.model.account.ExternalAccessToken; +import dev.cloudeko.zenei.domain.model.account.ExternalAccessTokenRepository; +import jakarta.enterprise.context.ApplicationScoped; +import lombok.AllArgsConstructor; + +import java.util.List; + +@ApplicationScoped +@AllArgsConstructor +public class ListExternalAccessTokensImpl implements ListExternalAccessTokens { + + private final ExternalAccessTokenMapper externalAccessTokenMapper; + private final ExternalAccessTokenRepository externalAccessTokenRepository; + + @Override + public List listByProvider(long userId, String provider) { + return externalAccessTokenRepository.listByProvider(userId, provider); + } +} diff --git a/core/src/main/java/dev/cloudeko/zenei/domain/feature/impl/LoginUserWithAuthorizationCodeImpl.java b/core/src/main/java/dev/cloudeko/zenei/domain/feature/impl/LoginUserWithAuthorizationCodeImpl.java index 20b39ff..802546d 100644 --- a/core/src/main/java/dev/cloudeko/zenei/domain/feature/impl/LoginUserWithAuthorizationCodeImpl.java +++ b/core/src/main/java/dev/cloudeko/zenei/domain/feature/impl/LoginUserWithAuthorizationCodeImpl.java @@ -8,7 +8,8 @@ import dev.cloudeko.zenei.domain.feature.util.TokenUtil; import dev.cloudeko.zenei.domain.model.Token; import dev.cloudeko.zenei.domain.model.account.Account; -import dev.cloudeko.zenei.domain.model.account.AccountRepository; +import dev.cloudeko.zenei.domain.model.account.ExternalAccessToken; +import dev.cloudeko.zenei.domain.model.account.ExternalAccountRepository; import dev.cloudeko.zenei.domain.model.email.EmailAddress; import dev.cloudeko.zenei.domain.model.token.RefreshTokenRepository; import dev.cloudeko.zenei.domain.model.user.User; @@ -20,7 +21,7 @@ import dev.cloudeko.zenei.extension.external.ExternalAuthResolver; import dev.cloudeko.zenei.extension.external.ExternalUserProfile; import dev.cloudeko.zenei.extension.external.providers.AvailableProvider; -import dev.cloudeko.zenei.extension.external.web.client.ExternalAccessToken; +import dev.cloudeko.zenei.extension.external.web.client.ExternalProviderAccessToken; import dev.cloudeko.zenei.extension.external.web.client.LoginOAuthClient; import dev.cloudeko.zenei.infrastructure.config.ApplicationConfig; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; @@ -39,7 +40,7 @@ public class LoginUserWithAuthorizationCodeImpl implements LoginUserWithAuthoriz private final ApplicationConfig config; private final UserRepository userRepository; - private final AccountRepository accountRepository; + private final ExternalAccountRepository externalAccountRepository; private final RefreshTokenRepository refreshTokenRepository; private final RefreshTokenProvider refreshTokenProvider; @@ -95,16 +96,20 @@ private Optional getEmailFromUserProfile( private User createUserFromExternalUserProfile(ExternalUserProfile externalUserProfile, ExternalUserProfile.ExternalUserEmail externalEmail, String provider, - ExternalAccessToken accessToken) { + ExternalProviderAccessToken accessToken) { final var emailAddress = EmailAddress.builder().email(externalEmail.email()).emailVerified(true).build(); - final var account = Account.builder() - .provider(provider) - .providerId(externalUserProfile.getId()) - .scope(accessToken.getScope()) + final var externalAccessToken = ExternalAccessToken.builder() .accessToken(accessToken.getAccessToken()) .refreshToken(accessToken.getRefreshToken()) .tokenType(accessToken.getTokenType()) + .scope(accessToken.getScope()) + .build(); + + final var account = Account.builder() + .provider(provider) + .providerId(externalUserProfile.getId()) + .accessTokens(new ArrayList<>(List.of(externalAccessToken))) .build(); final var user = User.builder() diff --git a/core/src/main/java/dev/cloudeko/zenei/domain/mapping/ExternalAccessTokenMapper.java b/core/src/main/java/dev/cloudeko/zenei/domain/mapping/ExternalAccessTokenMapper.java new file mode 100644 index 0000000..44711c6 --- /dev/null +++ b/core/src/main/java/dev/cloudeko/zenei/domain/mapping/ExternalAccessTokenMapper.java @@ -0,0 +1,19 @@ +package dev.cloudeko.zenei.domain.mapping; + +import dev.cloudeko.zenei.domain.model.account.ExternalAccessToken; +import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.ExternalAccessTokenEntity; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +import java.util.List; + +@Mapper(config = QuarkusMappingConfig.class) +public interface ExternalAccessTokenMapper { + List toDomainList(List entities); + + ExternalAccessToken toDomain(ExternalAccessTokenEntity entity); + + void updateDomainFromEntity(ExternalAccessTokenEntity entity, @MappingTarget ExternalAccessToken domain); + + ExternalAccessTokenEntity toEntity(ExternalAccessToken domain); +} diff --git a/core/src/main/java/dev/cloudeko/zenei/domain/model/account/Account.java b/core/src/main/java/dev/cloudeko/zenei/domain/model/account/Account.java index dc7d458..dcfcdfd 100644 --- a/core/src/main/java/dev/cloudeko/zenei/domain/model/account/Account.java +++ b/core/src/main/java/dev/cloudeko/zenei/domain/model/account/Account.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.List; @Data @Builder @@ -18,14 +19,8 @@ public class Account { private String provider; private String providerId; - private String refreshToken; - private String accessToken; - private Long accessTokenExpiresAt; + private List accessTokens; - private String tokenType; - private String scope; - - private String idToken; private String sessionState; private LocalDateTime createdAt; diff --git a/core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccessToken.java b/core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccessToken.java new file mode 100644 index 0000000..0f02c7f --- /dev/null +++ b/core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccessToken.java @@ -0,0 +1,19 @@ +package dev.cloudeko.zenei.domain.model.account; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExternalAccessToken { + private String idToken; + private String refreshToken; + private String accessToken; + private String tokenType; + private String scope; + private Long accessTokenExpiresAt; +} diff --git a/core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccessTokenRepository.java b/core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccessTokenRepository.java new file mode 100644 index 0000000..023301c --- /dev/null +++ b/core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccessTokenRepository.java @@ -0,0 +1,8 @@ +package dev.cloudeko.zenei.domain.model.account; + +import java.util.List; + +public interface ExternalAccessTokenRepository { + + List listByProvider(long userId, String provider); +} diff --git a/core/src/main/java/dev/cloudeko/zenei/domain/model/account/AccountRepository.java b/core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccountRepository.java similarity index 75% rename from core/src/main/java/dev/cloudeko/zenei/domain/model/account/AccountRepository.java rename to core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccountRepository.java index 56635d1..fced45d 100644 --- a/core/src/main/java/dev/cloudeko/zenei/domain/model/account/AccountRepository.java +++ b/core/src/main/java/dev/cloudeko/zenei/domain/model/account/ExternalAccountRepository.java @@ -2,6 +2,6 @@ import java.util.Optional; -public interface AccountRepository { +public interface ExternalAccountRepository { Optional findByProviderId(String providerId); } diff --git a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ExternalAccessTokenEntity.java b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ExternalAccessTokenEntity.java index 72784b1..0aaba37 100644 --- a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ExternalAccessTokenEntity.java +++ b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ExternalAccessTokenEntity.java @@ -1,4 +1,40 @@ package dev.cloudeko.zenei.infrastructure.repository.hibernate.entity; -public class ExternalAccessTokenEntity { +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "external_access_tokens") +@NamedQueries({ + @NamedQuery(name = "ExternalAccessTokenEntity.listByProvider", query = "SELECT e FROM ExternalAccessTokenEntity e WHERE e.account.user.id = :user AND e.account.provider = :provider") +}) +public class ExternalAccessTokenEntity extends PanacheEntity { + + @ManyToOne + @JoinColumn(name = "external_account_id", referencedColumnName = "id", nullable = false) + private ExternalAccountEntity account; + + @Column(name = "id_token") + private String idToken; + + @Column(name = "refresh_token") + private String refreshToken; + + @Column(name = "access_token") + private String accessToken; + + @Column(name = "token_type") + private String tokenType; + + @Column(name = "scope") + private String scope; + + @Column(name = "access_token_expires_at") + private Long accessTokenExpiresAt; } diff --git a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ExternalAccountEntity.java b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ExternalAccountEntity.java index c119a23..b04669a 100644 --- a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ExternalAccountEntity.java +++ b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ExternalAccountEntity.java @@ -9,6 +9,7 @@ import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDateTime; +import java.util.List; @Data @Entity @@ -34,23 +35,8 @@ public class ExternalAccountEntity extends PanacheEntity { @Column(name = "provider_id") private String providerId; - @Column(name = "refresh_token") - private String refreshToken; - - @Column(name = "access_token") - private String accessToken; - - @Column(name = "access_token_expires_at") - private Long accessTokenExpiresAt; - - @Column(name = "token_type") - private String tokenType; - - @Column(name = "scope") - private String scope; - - @Column(name = "id_token") - private String idToken; + @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true) + private List accessTokens; @Column(name = "session_state") private String sessionState; diff --git a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ExternalAccessTokenRepositoryPanache.java b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ExternalAccessTokenRepositoryPanache.java new file mode 100644 index 0000000..49b4054 --- /dev/null +++ b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ExternalAccessTokenRepositoryPanache.java @@ -0,0 +1,27 @@ +package dev.cloudeko.zenei.infrastructure.repository.hibernate.panache; + +import dev.cloudeko.zenei.domain.mapping.ExternalAccessTokenMapper; +import dev.cloudeko.zenei.domain.model.account.ExternalAccessToken; +import dev.cloudeko.zenei.domain.model.account.ExternalAccessTokenRepository; +import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.ExternalAccessTokenEntity; +import jakarta.enterprise.context.ApplicationScoped; +import lombok.AllArgsConstructor; + +import java.util.List; + +import static io.quarkus.panache.common.Parameters.with; + +@ApplicationScoped +@AllArgsConstructor +public class ExternalAccessTokenRepositoryPanache extends AbstractPanacheRepository implements + ExternalAccessTokenRepository { + + private final ExternalAccessTokenMapper externalAccessTokenMapper; + + @Override + public List listByProvider(long user, String provider) { + final var tokens = list("#ExternalAccessTokenEntity.listByProvider", with("user", user).and("provider", provider)); + + return externalAccessTokenMapper.toDomainList(tokens); + } +} diff --git a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/AccountRepositoryPanache.java b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ExternalAccountRepositoryPanache.java similarity index 81% rename from core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/AccountRepositoryPanache.java rename to core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ExternalAccountRepositoryPanache.java index 3936290..ca4d939 100644 --- a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/AccountRepositoryPanache.java +++ b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ExternalAccountRepositoryPanache.java @@ -2,7 +2,7 @@ import dev.cloudeko.zenei.domain.mapping.AccountMapper; import dev.cloudeko.zenei.domain.model.account.Account; -import dev.cloudeko.zenei.domain.model.account.AccountRepository; +import dev.cloudeko.zenei.domain.model.account.ExternalAccountRepository; import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.ExternalAccountEntity; import jakarta.enterprise.context.ApplicationScoped; import lombok.AllArgsConstructor; @@ -13,7 +13,8 @@ @ApplicationScoped @AllArgsConstructor -public class AccountRepositoryPanache extends AbstractPanacheRepository implements AccountRepository { +public class ExternalAccountRepositoryPanache extends AbstractPanacheRepository implements + ExternalAccountRepository { private final AccountMapper accountMapper; diff --git a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserRepositoryPanache.java b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserRepositoryPanache.java index a84549e..03fb7c4 100644 --- a/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserRepositoryPanache.java +++ b/core/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserRepositoryPanache.java @@ -25,6 +25,9 @@ public void createUser(User user) { userEntity.getEmailAddresses().forEach(emailAddressEntity -> emailAddressEntity.setUser(userEntity)); userEntity.getAccounts().forEach(accountEntity -> accountEntity.setUser(userEntity)); + userEntity.getAccounts().forEach(accountEntity -> accountEntity.getAccessTokens() + .forEach(accessTokenEntity -> accessTokenEntity.setAccount(accountEntity))); + persistAndFlush(userEntity); userMapper.updateDomainFromEntity(userEntity, user); diff --git a/core/src/main/resources/application-test.properties b/core/src/main/resources/application-test.properties index 3eb2eed..adccf77 100644 --- a/core/src/main/resources/application-test.properties +++ b/core/src/main/resources/application-test.properties @@ -9,10 +9,4 @@ zenei.jwt.private.key.location=dev-private-key.pem # Location of the public key file for JWT verification. zenei.jwt.public.key.location=dev-public-key.pem # Issuer identifier to be included in generated JWTs. -zenei.jwt.issuer=https://example.com/issuer - -# Default zenei admin user -zenei.user.default.admin.username=admin -zenei.user.default.admin.email=admin@test.com -zenei.user.default.admin.password=test -zenei.user.default.admin.role=admin \ No newline at end of file +zenei.jwt.issuer=https://example.com/issuer \ No newline at end of file diff --git a/core/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowWithAdminUserTest.java b/core/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowWithAdminUserTest.java index f3e0b30..fa898b7 100644 --- a/core/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowWithAdminUserTest.java +++ b/core/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowWithAdminUserTest.java @@ -37,9 +37,9 @@ void testGetUserInfo() { .then() .statusCode(Response.Status.OK.getStatusCode()) .body( - "users.size()", greaterThanOrEqualTo(1), - "users.getFirst().username", equalTo("admin"), - "users.getFirst().primary_email_address", equalTo("admin@test.com") + "size()", greaterThanOrEqualTo(1), + "getFirst().username", equalTo("admin"), + "getFirst().primary_email_address", equalTo("admin@test.com") ); } } diff --git a/core/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowWithExternalProviderTest.java b/core/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowWithExternalProviderTest.java index 1de598b..eb9e8bf 100644 --- a/core/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowWithExternalProviderTest.java +++ b/core/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowWithExternalProviderTest.java @@ -1,5 +1,7 @@ package dev.cloudeko.zenei.auth; +import dev.cloudeko.zenei.application.web.model.response.ExternalAccessTokensResponse; +import dev.cloudeko.zenei.application.web.model.response.PrivateUsersResponse; import dev.cloudeko.zenei.extension.external.providers.AvailableProvider; import dev.cloudeko.zenei.resource.MockDiscordAuthorizationServerTestResource; import dev.cloudeko.zenei.resource.MockGithubAuthorizationServerTestResource; @@ -7,10 +9,10 @@ import dev.cloudeko.zenei.resource.MockServerResource; import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; import io.restassured.RestAssured; import jakarta.ws.rs.core.Response; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -19,8 +21,10 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; @QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @WithTestResource.List({ @WithTestResource(MockServerResource.class), @WithTestResource(MockGithubAuthorizationServerTestResource.class), @@ -36,6 +40,7 @@ static void setup() { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); } + @Order(1) @MethodSource("createProviderData") @ParameterizedTest(name = "Test Case for provider: {0}") @DisplayName("Retrieve a access token using authorization (GET /external/login/) should return (200 OK)") @@ -49,6 +54,32 @@ void testGetUserInfo(String provider) { ); } + @Test + @Order(2) + @TestSecurity(user = "admin", roles = "admin") + @DisplayName("Retrieve a access token using authorization (GET /admin/users/{userId}/external_access_tokens/{provider}) should return (200 OK)") + void testGetExternalAccessToken() { + final var users = given() + .get("/admin/users") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(PrivateUsersResponse.class); + + assertEquals(createProviderData().count(), users.size()); + + users.forEach(user -> { + final var tokens = given() + .get("/admin/users/" + user.getId() + "/external_access_tokens/" + user.getUsername() + .substring(0, user.getUsername().indexOf("-"))) + + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(ExternalAccessTokensResponse.class); + + assertEquals(1, tokens.size()); + }); + } + static Stream createProviderData() { return Stream.of(AvailableProvider.values()) .filter(provider -> !isIgnoredProvider(provider)) diff --git a/core/src/test/java/dev/cloudeko/zenei/resource/TestProviderFeature.java b/core/src/test/java/dev/cloudeko/zenei/resource/TestProviderFeature.java index 6627b89..b13168b 100644 --- a/core/src/test/java/dev/cloudeko/zenei/resource/TestProviderFeature.java +++ b/core/src/test/java/dev/cloudeko/zenei/resource/TestProviderFeature.java @@ -1,13 +1,13 @@ package dev.cloudeko.zenei.resource; -import dev.cloudeko.zenei.extension.external.web.client.ExternalAccessToken; +import dev.cloudeko.zenei.extension.external.web.client.ExternalProviderAccessToken; import lombok.Getter; @Getter public enum TestProviderFeature { AUTHORIZE("/login/oauth/authorize"), - ACCESS_TOKEN("/login/oauth/access_token", new ExternalAccessToken("mock_access_token", + ACCESS_TOKEN("/login/oauth/access_token", new ExternalProviderAccessToken("mock_access_token", 3600L, "mock_refresh_token", "user,email", diff --git a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/ExternalAuthProvider.java b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/ExternalAuthProvider.java index e70f938..11288d1 100644 --- a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/ExternalAuthProvider.java +++ b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/ExternalAuthProvider.java @@ -1,10 +1,10 @@ package dev.cloudeko.zenei.extension.external; import dev.cloudeko.zenei.extension.external.config.ExternalAuthProviderConfig; -import dev.cloudeko.zenei.extension.external.web.client.ExternalAccessToken; +import dev.cloudeko.zenei.extension.external.web.client.ExternalProviderAccessToken; public interface ExternalAuthProvider { - ExternalUserProfile getExternalUserProfile(ExternalAccessToken accessToken); + ExternalUserProfile getExternalUserProfile(ExternalProviderAccessToken accessToken); ExternalAuthProviderConfig config(); diff --git a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/DiscordExternalAuthProvider.java b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/DiscordExternalAuthProvider.java index 97773ac..dfcbc60 100644 --- a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/DiscordExternalAuthProvider.java +++ b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/DiscordExternalAuthProvider.java @@ -4,7 +4,7 @@ import dev.cloudeko.zenei.extension.external.ExternalUserProfile; import dev.cloudeko.zenei.extension.external.config.ExternalAuthProviderConfig; import dev.cloudeko.zenei.extension.external.endpoint.ProviderEndpoints; -import dev.cloudeko.zenei.extension.external.web.client.ExternalAccessToken; +import dev.cloudeko.zenei.extension.external.web.client.ExternalProviderAccessToken; import dev.cloudeko.zenei.extension.external.web.external.discord.DiscordClient; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; @@ -14,7 +14,7 @@ public record DiscordExternalAuthProvider(ExternalAuthProviderConfig config) implements ExternalAuthProvider { @Override - public ExternalUserProfile getExternalUserProfile(ExternalAccessToken accessToken) { + public ExternalUserProfile getExternalUserProfile(ExternalProviderAccessToken accessToken) { final var client = QuarkusRestClientBuilder.newBuilder() .baseUri(URI.create(getBaseEndpoint())) .build(DiscordClient.class); diff --git a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/GithubExternalAuthProvider.java b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/GithubExternalAuthProvider.java index a279dc1..927396a 100644 --- a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/GithubExternalAuthProvider.java +++ b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/GithubExternalAuthProvider.java @@ -4,7 +4,7 @@ import dev.cloudeko.zenei.extension.external.ExternalUserProfile; import dev.cloudeko.zenei.extension.external.config.ExternalAuthProviderConfig; import dev.cloudeko.zenei.extension.external.endpoint.ProviderEndpoints; -import dev.cloudeko.zenei.extension.external.web.client.ExternalAccessToken; +import dev.cloudeko.zenei.extension.external.web.client.ExternalProviderAccessToken; import dev.cloudeko.zenei.extension.external.web.external.github.GithubClient; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; @@ -13,7 +13,7 @@ public record GithubExternalAuthProvider(ExternalAuthProviderConfig config) implements ExternalAuthProvider { @Override - public ExternalUserProfile getExternalUserProfile(ExternalAccessToken accessToken) { + public ExternalUserProfile getExternalUserProfile(ExternalProviderAccessToken accessToken) { final var client = QuarkusRestClientBuilder.newBuilder() .baseUri(URI.create(getBaseEndpoint())) .build(GithubClient.class); diff --git a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/GoogleExternalAuthProvider.java b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/GoogleExternalAuthProvider.java index c866bb9..83730db 100644 --- a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/GoogleExternalAuthProvider.java +++ b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/providers/GoogleExternalAuthProvider.java @@ -4,7 +4,7 @@ import dev.cloudeko.zenei.extension.external.ExternalUserProfile; import dev.cloudeko.zenei.extension.external.config.ExternalAuthProviderConfig; import dev.cloudeko.zenei.extension.external.endpoint.ProviderEndpoints; -import dev.cloudeko.zenei.extension.external.web.client.ExternalAccessToken; +import dev.cloudeko.zenei.extension.external.web.client.ExternalProviderAccessToken; import dev.cloudeko.zenei.extension.external.web.external.google.GoogleClient; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; @@ -14,7 +14,7 @@ public record GoogleExternalAuthProvider(ExternalAuthProviderConfig config) implements ExternalAuthProvider { @Override - public ExternalUserProfile getExternalUserProfile(ExternalAccessToken accessToken) { + public ExternalUserProfile getExternalUserProfile(ExternalProviderAccessToken accessToken) { final var client = QuarkusRestClientBuilder.newBuilder() .baseUri(URI.create(getBaseEndpoint())) .build(GoogleClient.class); diff --git a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/web/client/ExternalAccessToken.java b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/web/client/ExternalAccessToken.java deleted file mode 100644 index ee3c831..0000000 --- a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/web/client/ExternalAccessToken.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.cloudeko.zenei.extension.external.web.client; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ExternalAccessToken { - - @JsonProperty("access_token") - private String accessToken; - - @JsonProperty("expires_in") - private Long expiresIn; - - @JsonProperty("refresh_token") - private String refreshToken; - - @JsonProperty("scope") - private String scope; - - @JsonProperty("token_type") - private String tokenType; -} diff --git a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/web/client/LoginOAuthClient.java b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/web/client/LoginOAuthClient.java index 91bcf44..24f85bc 100644 --- a/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/web/client/LoginOAuthClient.java +++ b/extensions/external-authentication/runtime/src/main/java/dev/cloudeko/zenei/extension/external/web/client/LoginOAuthClient.java @@ -11,7 +11,7 @@ public interface LoginOAuthClient { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @ClientHeaderParam(name = "Accept", value = MediaType.APPLICATION_JSON) - ExternalAccessToken getAccessToken(@FormParam("grant_type") String grantType, + ExternalProviderAccessToken getAccessToken(@FormParam("grant_type") String grantType, @FormParam("client_id") String clientId, @FormParam("client_secret") String clientSecret, @FormParam("code") String code,