diff --git a/src/main/java/com/odeyalo/sonata/connect/config/profiles/local/AuthenticationConfiguration.java b/src/main/java/com/odeyalo/sonata/connect/config/profiles/local/AuthenticationConfiguration.java new file mode 100644 index 0000000..66043ae --- /dev/null +++ b/src/main/java/com/odeyalo/sonata/connect/config/profiles/local/AuthenticationConfiguration.java @@ -0,0 +1,72 @@ +package com.odeyalo.sonata.connect.config.profiles.local; + +import com.odeyalo.suite.security.auth.TokenAuthenticationManager; +import com.odeyalo.suite.security.auth.token.AccessTokenMetadata; +import com.odeyalo.suite.security.auth.token.ReactiveAccessTokenValidator; +import com.odeyalo.suite.security.auth.token.ValidatedAccessToken; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.Map; + +@Configuration +@Profile("local") +public class AuthenticationConfiguration { + + @Bean + @Primary + public ReactiveAuthenticationManager reactiveAuthenticationManager() { + return new TokenAuthenticationManager( + new LocalDevelopmentAccessTokenValidator() + ); + } + + private static class LocalDevelopmentAccessTokenValidator implements ReactiveAccessTokenValidator { + private final Map tokensCache = Map.of( + "token1", ValidatedAccessToken.valid( + AccessTokenMetadata.of("123", new String[]{"read", "write"}, + Instant.now().getEpochSecond(), + Instant.now().plusSeconds(600).getEpochSecond() + )), + "token1_2", ValidatedAccessToken.valid( + AccessTokenMetadata.of("123", new String[]{"read", "write", "playlist"}, + Instant.now().getEpochSecond(), + Instant.now().plusSeconds(600).getEpochSecond() + )), + "token2", ValidatedAccessToken.valid( + AccessTokenMetadata.of("miku", new String[]{"read", "write", "playlist"}, + Instant.now().getEpochSecond(), + Instant.now().plusSeconds(600).getEpochSecond() + )) + ); + + private final Logger logger = LoggerFactory.getLogger(LocalDevelopmentAccessTokenValidator.class); + + public LocalDevelopmentAccessTokenValidator() { + logger.info("Using local 'DEV' mode, cache of available access tokens to use. Token that is not exist in cache will cause HTTP 401 UNAUTHORIZED status"); + + tokensCache.forEach((key, value) -> { + final AccessTokenMetadata tokenMetadata = value.getToken(); + logger.info("Generated access token: '{}' for user 'ID({})' with following scopes: '({})', expire after: {} seconds", key, tokenMetadata.getUserId(), tokenMetadata.getScopes(), tokenMetadata.getExpiresIn() - Instant.now().getEpochSecond()); + }); + + } + + @Override + @NotNull + public Mono validateToken(@NotNull final String tokenValue) { + + final ValidatedAccessToken validatedAccessToken = tokensCache.getOrDefault(tokenValue, ValidatedAccessToken.invalid()); + + return Mono.just(validatedAccessToken); + } + } +} diff --git a/src/main/java/com/odeyalo/sonata/connect/config/profiles/local/PlayableItemsConfig.java b/src/main/java/com/odeyalo/sonata/connect/config/profiles/local/PlayableItemsConfig.java new file mode 100644 index 0000000..1a99e12 --- /dev/null +++ b/src/main/java/com/odeyalo/sonata/connect/config/profiles/local/PlayableItemsConfig.java @@ -0,0 +1,84 @@ +package com.odeyalo.sonata.connect.config.profiles.local; + +import com.odeyalo.sonata.common.context.ContextUri; +import com.odeyalo.sonata.connect.model.*; +import com.odeyalo.sonata.connect.model.track.*; +import com.odeyalo.sonata.connect.service.player.support.PlayableItemLoader; +import com.odeyalo.sonata.connect.service.player.support.PredefinedPlayableItemLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import java.net.URI; +import java.util.List; + +@Configuration +@Profile("local") +public class PlayableItemsConfig { + private final Logger logger = LoggerFactory.getLogger(PlayableItemsConfig.class); + + @Bean + public PlayableItemLoader playableItemLoader() { + ArtistList artists = ArtistList.solo( + Artist.of(ArtistSpec.ArtistId.of("fea3aFas3f"), "Alex G", ContextUri.forArtist("fea3aFas3f")) + ); + + ImageList images = ImageList.builder() + .image(Image.builder().url(URI.create("https://i.pinimg.com/564x/02/27/b0/0227b0ff5ff93d6429d2c80d402cea43.jpg")).build()) + .image(Image.builder().url(URI.create("https://i.pinimg.com/564x/db/ff/9f/dbff9f74ef082687010dacc455eac7ac.jpg")).build()) + .build(); + + Album albumInfo = Album.builder() + .id(AlbumSpec.AlbumId.of("a3la23bu91m")) + .artists(artists) + .totalTrackCount(2) + .albumType(AlbumSpec.AlbumType.EPISODE) + .name("Sarah") + .images(images) + .build(); + + var item = TrackItem.builder() + .id("04nJixim5a0MAz3PGiVID1") + .contextUri(ContextUri.forTrack("04nJixim5a0MAz3PGiVID1")) + .name("Something") + .explicit(true) + .duration(PlayableItemDuration.ofMilliseconds(790024)) + .artists(artists) + .order(TrackItemSpec.Order.of(1, 1)) + .album(albumInfo) + .build(); + + ImageList images2 = ImageList.builder() + .image(Image.builder().url(URI.create("https://i.pinimg.com/564x/90/bc/a8/90bca83aa94a664206a7e4c305888023.jpg")).build()) + .build(); + + + Album albumInfo2 = Album.builder() + .id(AlbumSpec.AlbumId.of("miku123")) + .artists(artists) + .totalTrackCount(2) + .albumType(AlbumSpec.AlbumType.EPISODE) + .name("Sarah") + .images(images2) + .build(); + + var item2 = TrackItem.builder() + .id("miku123") + .contextUri(ContextUri.forTrack("miku123")) + .name("Something") + .explicit(true) + .duration(PlayableItemDuration.ofMilliseconds(790024)) + .artists(artists) + .order(TrackItemSpec.Order.of(1, 1)) + .album(albumInfo2) + .build(); + + logger.info("Created new playable items for local dev only with ids: {}, {}", item.getId(), item2.getId()); + + return new PredefinedPlayableItemLoader(List.of( + item, item2 + )); + } +} diff --git a/src/main/java/com/odeyalo/sonata/connect/config/security/configurer/AuthorizeExchangeSpecConfigurer.java b/src/main/java/com/odeyalo/sonata/connect/config/security/configurer/AuthorizeExchangeSpecConfigurer.java index bb5488f..56d0501 100644 --- a/src/main/java/com/odeyalo/sonata/connect/config/security/configurer/AuthorizeExchangeSpecConfigurer.java +++ b/src/main/java/com/odeyalo/sonata/connect/config/security/configurer/AuthorizeExchangeSpecConfigurer.java @@ -1,5 +1,6 @@ package com.odeyalo.sonata.connect.config.security.configurer; +import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec; import org.springframework.stereotype.Component; @@ -8,13 +9,14 @@ * Helper class to configure the {@link AuthorizeExchangeSpec} */ @Component -public class AuthorizeExchangeSpecConfigurer implements Customizer { +public final class AuthorizeExchangeSpecConfigurer implements Customizer { private static final String SCA_TOKEN_EXCHANGE_ENDPOINT = "/connect/auth/exchange**"; @Override public void customize(AuthorizeExchangeSpec authorizeExchangeSpec) { authorizeExchangeSpec .pathMatchers(SCA_TOKEN_EXCHANGE_ENDPOINT).permitAll() + .pathMatchers(HttpMethod.OPTIONS).permitAll() .anyExchange().authenticated(); } } diff --git a/src/main/java/com/odeyalo/sonata/connect/config/security/configurer/CorsSpecConfigurer.java b/src/main/java/com/odeyalo/sonata/connect/config/security/configurer/CorsSpecConfigurer.java index 8ba25ac..9b6fb4e 100644 --- a/src/main/java/com/odeyalo/sonata/connect/config/security/configurer/CorsSpecConfigurer.java +++ b/src/main/java/com/odeyalo/sonata/connect/config/security/configurer/CorsSpecConfigurer.java @@ -3,6 +3,8 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.web.server.ServerHttpSecurity.CorsSpec; import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; /** * Used to configure the {@link CorsSpec} @@ -12,6 +14,12 @@ public class CorsSpecConfigurer implements Customizer { @Override public void customize(CorsSpec corsSpec) { - corsSpec.disable(); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedMethod("*"); + config.applyPermitDefaultValues(); + source.registerCorsConfiguration("/**", config); + + corsSpec.configurationSource(source); } } diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties new file mode 100644 index 0000000..e15d969 --- /dev/null +++ b/src/main/resources/application-local.properties @@ -0,0 +1,4 @@ + +spring.webflux.base-path=/v1 + +eureka.client.enabled=false \ No newline at end of file