Skip to content

Commit

Permalink
feat(provider): added google provider
Browse files Browse the repository at this point in the history
  • Loading branch information
zZHorizonZz committed Aug 15, 2024
1 parent 34fe3cb commit e44419d
Show file tree
Hide file tree
Showing 23 changed files with 394 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import dev.cloudeko.zenei.domain.feature.LoginUserWithAuthorizationCode;
import dev.cloudeko.zenei.extension.external.ExternalAuthProvider;
import dev.cloudeko.zenei.extension.external.ExternalAuthResolver;
import dev.cloudeko.zenei.extension.external.providers.AvailableProvider;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
Expand Down Expand Up @@ -36,7 +37,7 @@ public Response login(@PathParam("provider") String provider) {

final var uriBuilder = UriBuilder.fromUri(providerConfig.get().getAuthorizationEndpoint())
.queryParam("client_id", providerConfig.get().config().clientId())
.queryParam("redirect_uri", providerConfig.get().config().redirectUri().orElse(""))
.queryParam("redirect_uri", AvailableProvider.getProvider(provider).getRedirectUri())
.queryParam("response_type", "code")
.queryParam("scope", providerConfig.get().config().scope().orElse(""));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import dev.cloudeko.zenei.extension.external.ExternalAuthProvider;
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.LoginOAuthClient;
import dev.cloudeko.zenei.infrastructure.config.ApplicationConfig;
Expand Down Expand Up @@ -57,8 +58,7 @@ public Token handle(String provider, String code) {
final var accessToken = client.getAccessToken("authorization_code",
externalProvider.config().clientId(),
externalProvider.config().clientSecret(),
code,
externalProvider.config().redirectUri().orElseThrow());
code, AvailableProvider.getProvider(provider).getRedirectUri());

if (accessToken == null) {
throw new IllegalArgumentException("Invalid authorization code");
Expand Down

This file was deleted.

16 changes: 7 additions & 9 deletions core/src/main/resources/application-providers.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@
zenei.external.auth.providers.github.enabled=true
zenei.external.auth.providers.github.client-id=<YOUR_CLIENT_ID>
zenei.external.auth.providers.github.client-secret=<YOUR_CLIENT_SECRET>
zenei.external.auth.providers.github.authorization-uri=https://github.com/login/oauth/authorize
zenei.external.auth.providers.github.token-uri=https://github.com/login/oauth/access_token
zenei.external.auth.providers.github.base-uri=https://api.github.com
zenei.external.auth.providers.github.redirect-uri=http://localhost:8080/external/callback/github
zenei.external.auth.providers.github.scope=user:email

# Discord OAuth
zenei.external.auth.providers.discord.enabled=true
zenei.external.auth.providers.discord.client-id=<YOUR_CLIENT_ID>
zenei.external.auth.providers.discord.client-secret=<YOUR_CLIENT_SECRET>
zenei.external.auth.providers.discord.authorization-uri=https://discord.com/api/oauth2/authorize
zenei.external.auth.providers.discord.token-uri=https://discord.com/api/oauth2/token
zenei.external.auth.providers.discord.base-uri=https://discord.com/api
zenei.external.auth.providers.discord.redirect-uri=http://localhost:8080/external/callback/discord
zenei.external.auth.providers.discord.scope=identify email
zenei.external.auth.providers.discord.scope=identify email

# Google OAuth
zenei.external.auth.providers.google.enabled=true
zenei.external.auth.providers.google.client-id=<YOUR_CLIENT_ID>
zenei.external.auth.providers.google.client-secret=<YOUR_CLIENT_SECRET>
zenei.external.auth.providers.google.scope=openid email profile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package dev.cloudeko.zenei.auth;

import dev.cloudeko.zenei.extension.external.providers.AvailableProvider;
import dev.cloudeko.zenei.resource.MockDiscordAuthorizationServerTestResource;
import dev.cloudeko.zenei.resource.MockGithubAuthorizationServerTestResource;
import dev.cloudeko.zenei.resource.MockGoogleAuthorizationServerTestResource;
import dev.cloudeko.zenei.resource.MockServerResource;
import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.QuarkusTest;
Expand All @@ -22,18 +24,21 @@
@WithTestResource.List({
@WithTestResource(MockServerResource.class),
@WithTestResource(MockGithubAuthorizationServerTestResource.class),
@WithTestResource(MockDiscordAuthorizationServerTestResource.class)
@WithTestResource(MockDiscordAuthorizationServerTestResource.class),
@WithTestResource(MockGoogleAuthorizationServerTestResource.class)
})
public class AuthenticationFlowWithExternalProviderTest {

private static final String[] IGNORED_PROVIDERS = { null };

@BeforeAll
static void setup() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}

@MethodSource("createProviderData")
@ParameterizedTest(name = "Test Case for provider: {0}")
@DisplayName("Retrieve a access token using authorization (GET /external/login/github) should return (200 OK)")
@DisplayName("Retrieve a access token using authorization (GET /external/login/<PROVIDER>) should return (200 OK)")
void testGetUserInfo(String provider) {
given().get("/external/login/" + provider)
.then()
Expand All @@ -45,6 +50,17 @@ void testGetUserInfo(String provider) {
}

static Stream<Arguments> createProviderData() {
return Stream.of(Arguments.of("github"), Arguments.of("discord"));
return Stream.of(AvailableProvider.values())
.filter(provider -> !isIgnoredProvider(provider))
.map(provider -> Arguments.of(provider.name()));
}

private static boolean isIgnoredProvider(AvailableProvider provider) {
for (String ignoredProvider : IGNORED_PROVIDERS) {
if (provider.name().equalsIgnoreCase(ignoredProvider)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,86 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.ConfigProvider;

import java.util.Map;
import java.util.Set;

public abstract class AbstractMockAuthorizationServerTestResource implements QuarkusTestResourceLifecycleManager {

protected final ObjectMapper objectMapper = new ObjectMapper();

@Override
public Map<String, String> start() {
WireMockServer wireMockServer = MockServerResource.getWireMockServer();
return providerSpecificStubsAndConfig(wireMockServer);
final var server = MockServerResource.getWireMockServer();

try {
if (isFeatureEnabled(TestProviderFeature.AUTHORIZE)) {
// Mock the authorization URL
server.stubFor(WireMock.get(WireMock.urlPathEqualTo(getFeaturePath(TestProviderFeature.AUTHORIZE)))
.withQueryParam("client_id", WireMock.matching(".*"))
.withQueryParam("redirect_uri", WireMock.matching(".*"))
.withQueryParam("scope", WireMock.matching(".*"))
.willReturn(WireMock.aResponse()
.withStatus(Response.Status.FOUND.getStatusCode())
.withHeader("Location", getCallbackEndpoint() + "?code=mock_code&state=mock_state")));
}

if (isFeatureEnabled(TestProviderFeature.ACCESS_TOKEN)) {
// Mock the access token endpoint
server.stubFor(WireMock.post(WireMock.urlPathEqualTo(getFeaturePath(TestProviderFeature.ACCESS_TOKEN)))
.withFormParam("client_id", WireMock.matching(".*"))
.withFormParam("client_secret", WireMock.matching(".*"))
.withFormParam("code", WireMock.matching(".*"))
.withFormParam("grant_type", WireMock.matching(".*"))
.withFormParam("redirect_uri", WireMock.matching(".*"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON)
.withBody(objectMapper.writeValueAsString(
getFeatureResponse(TestProviderFeature.ACCESS_TOKEN)))));
}
} catch (Exception e) {
throw new RuntimeException(e);
}

return providerSpecificStubsAndConfig(server);
}

@Override
public void stop() {
}

protected int getTestPort() {
return ConfigProvider.getConfig().getOptionalValue("quarkus.http.test-port", Integer.class).orElse(8081);
}

protected boolean isFeatureEnabled(TestProviderFeature feature) {
return getFeatures().contains(feature);
}

protected String getFeaturePath(TestProviderFeature feature) {
return getPrefix() + feature.getPath();
}

protected Object getFeatureResponse(TestProviderFeature feature) {
return feature.getResponse();
}

protected abstract Map<String, String> providerSpecificStubsAndConfig(WireMockServer server);

protected abstract Set<TestProviderFeature> getFeatures();

protected abstract String getProvider();

protected String getPrefix() {
return String.format("/%s", getProvider());
}

protected String getCallbackEndpoint() {
return "http://localhost:" + getTestPort() + "/external/callback/" + getProvider();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,27 @@

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import dev.cloudeko.zenei.extension.external.web.client.ExternalAccessToken;
import dev.cloudeko.zenei.extension.external.web.external.discord.DiscordUser;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.ConfigProvider;
import jakarta.ws.rs.core.MediaType;

import java.util.Map;
import java.util.Set;

public class MockDiscordAuthorizationServerTestResource extends AbstractMockAuthorizationServerTestResource {

private static final DiscordUser DISCORD_USER = new DiscordUser("12345L", "discord-user", "Discord Test User", "1234",
"https://example.com/avatar.jpg",
"[email protected]", true);

@Override
protected Map<String, String> providerSpecificStubsAndConfig(WireMockServer server) {
final var testPort = ConfigProvider.getConfig().getOptionalValue("quarkus.http.test-port", Integer.class).orElse(8081);

try {
// Mock the Discord authorization URL
server.stubFor(WireMock.get(WireMock.urlPathEqualTo("/discord/login/oauth/authorize"))
.withQueryParam("client_id", WireMock.matching(".*"))
.withQueryParam("redirect_uri", WireMock.matching(".*"))
.withQueryParam("scope", WireMock.matching(".*"))
.willReturn(WireMock.aResponse()
.withStatus(Response.Status.FOUND.getStatusCode())
.withHeader("Location",
"http://localhost:" + testPort + "/external/callback/discord?code=mock_code&state=mock_state")));

// Mock the Discord access token endpoint
server.stubFor(WireMock.post(WireMock.urlPathEqualTo("/discord/login/oauth/access_token"))
.withFormParam("client_id", WireMock.matching(".*"))
.withFormParam("client_secret", WireMock.matching(".*"))
.withFormParam("code", WireMock.matching(".*"))
.withFormParam("grant_type", WireMock.matching(".*"))
.withFormParam("redirect_uri", WireMock.matching(".*"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", "application/json")
.withBody(objectMapper.writeValueAsString(
new ExternalAccessToken("mock_access_token", 3600L, "mock_refresh_token", "user,email",
"bearer")
))));

// Mock the Discord user data endpoints
server.stubFor(WireMock.get(WireMock.urlPathEqualTo("/discord/users/@me"))
.withHeader("Authorization", WireMock.equalTo("Bearer mock_access_token"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", "application/json")
.withBody(objectMapper.writeValueAsString(
new DiscordUser("12345L", "discord-user", "Discord Test User", "1234", "https://example.com/avatar.jpg",
"[email protected]", true)
))));
.withHeader("Content-Type", MediaType.APPLICATION_JSON)
.withBody(objectMapper.writeValueAsString(DISCORD_USER))));
} catch (Exception e) {
throw new RuntimeException(e);
}
Expand All @@ -58,11 +32,21 @@ protected Map<String, String> providerSpecificStubsAndConfig(WireMockServer serv
"zenei.external.auth.providers.discord.client-id", "mock_client_id",
"zenei.external.auth.providers.discord.client-secret", "mock_client_secret",
"zenei.external.auth.providers.discord.authorization-uri",
server.baseUrl() + "/discord/login/oauth/authorize",
server.baseUrl() + getFeaturePath(TestProviderFeature.AUTHORIZE),
"zenei.external.auth.providers.discord.token-uri",
server.baseUrl() + "/discord/login/oauth/access_token",
"zenei.external.auth.providers.discord.redirect-uri", "http://localhost:8081/external/callback/discord",
server.baseUrl() + getFeaturePath(TestProviderFeature.ACCESS_TOKEN),
"zenei.external.auth.providers.discord.redirect-uri", getCallbackEndpoint(),
"zenei.external.auth.providers.discord.scope", "user,email"
);
}

@Override
protected Set<TestProviderFeature> getFeatures() {
return Set.of(TestProviderFeature.AUTHORIZE, TestProviderFeature.ACCESS_TOKEN);
}

@Override
protected String getProvider() {
return "discord";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,18 @@

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import dev.cloudeko.zenei.extension.external.web.client.ExternalAccessToken;
import dev.cloudeko.zenei.extension.external.web.external.github.GithubUser;
import dev.cloudeko.zenei.extension.external.web.external.github.GithubUserEmail;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.ConfigProvider;

import java.util.List;
import java.util.Map;
import java.util.Set;

public class MockGithubAuthorizationServerTestResource extends AbstractMockAuthorizationServerTestResource {

@Override
protected Map<String, String> providerSpecificStubsAndConfig(WireMockServer server) {
final var testPort = ConfigProvider.getConfig().getOptionalValue("quarkus.http.test-port", Integer.class).orElse(8081);

try {
// Mock the GitHub authorization URL
server.stubFor(WireMock.get(WireMock.urlPathEqualTo("/github/login/oauth/authorize"))
.withQueryParam("client_id", WireMock.matching(".*"))
.withQueryParam("redirect_uri", WireMock.matching(".*"))
.withQueryParam("scope", WireMock.matching(".*"))
.willReturn(WireMock.aResponse()
.withStatus(Response.Status.FOUND.getStatusCode())
.withHeader("Location",
"http://localhost:" + testPort + "/external/callback/github?code=mock_code&state=mock_state")));

// Mock the access token endpoint
server.stubFor(WireMock.post(WireMock.urlPathEqualTo("/github/login/oauth/access_token"))
.withFormParam("client_id", WireMock.matching(".*"))
.withFormParam("client_secret", WireMock.matching(".*"))
.withFormParam("code", WireMock.matching(".*"))
.withFormParam("grant_type", WireMock.matching(".*"))
.withFormParam("redirect_uri", WireMock.matching(".*"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", "application/json")
.withBody(objectMapper.writeValueAsString(
new ExternalAccessToken("mock_access_token", 3600L, "mock_refresh_token", "user,email",
"bearer")
))));

// Mock the GitHub user data endpoints
server.stubFor(WireMock.get(WireMock.urlPathEqualTo("/github/user"))
.withHeader("Authorization", WireMock.equalTo("Bearer mock_access_token"))
Expand Down Expand Up @@ -70,10 +42,22 @@ protected Map<String, String> providerSpecificStubsAndConfig(WireMockServer serv
"zenei.external.auth.providers.github.base-uri", server.baseUrl() + "/github",
"zenei.external.auth.providers.github.client-id", "mock_client_id",
"zenei.external.auth.providers.github.client-secret", "mock_client_secret",
"zenei.external.auth.providers.github.authorization-uri", server.baseUrl() + "/github/login/oauth/authorize",
"zenei.external.auth.providers.github.token-uri", server.baseUrl() + "/github/login/oauth/access_token",
"zenei.external.auth.providers.github.redirect-uri", "http://localhost:8081/external/callback/github",
"zenei.external.auth.providers.github.authorization-uri",
server.baseUrl() + getFeaturePath(TestProviderFeature.AUTHORIZE),
"zenei.external.auth.providers.github.token-uri",
server.baseUrl() + getFeaturePath(TestProviderFeature.ACCESS_TOKEN),
"zenei.external.auth.providers.github.redirect-uri", getCallbackEndpoint(),
"zenei.external.auth.providers.github.scope", "user,email"
);
}

@Override
protected Set<TestProviderFeature> getFeatures() {
return Set.of(TestProviderFeature.AUTHORIZE, TestProviderFeature.ACCESS_TOKEN);
}

@Override
protected String getProvider() {
return "github";
}
}
Loading

0 comments on commit e44419d

Please sign in to comment.