Skip to content

Commit

Permalink
feat(external): added discord provider
Browse files Browse the repository at this point in the history
  • Loading branch information
zZHorizonZz committed Aug 14, 2024
1 parent 455a0f8 commit 246dab8
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.cloudeko.zenei.infrastructure.config;

public enum AvailableProviders {
GITHUB("github"),
DISCORD("discord");

private final String providerName;

AvailableProviders(String providerName) {
this.providerName = providerName;
}

public String getProviderName() {
return providerName;
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
package dev.cloudeko.zenei.auth;

import dev.cloudeko.zenei.application.web.model.response.TokenResponse;
import dev.cloudeko.zenei.resource.MockDiscordAuthorizationServerTestResource;
import dev.cloudeko.zenei.resource.MockGithubAuthorizationServerTestResource;
import dev.cloudeko.zenei.resource.MockServerResource;
import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.QuarkusTest;
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.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.notNullValue;

@QuarkusTest
@WithTestResource(MockGithubAuthorizationServerTestResource.class)
@WithTestResource.List({
@WithTestResource(MockServerResource.class),
@WithTestResource(MockGithubAuthorizationServerTestResource.class),
@WithTestResource(MockDiscordAuthorizationServerTestResource.class)
})
public class AuthenticationFlowWithExternalProviderTest {

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

@Test
@MethodSource("createProviderData")
@ParameterizedTest(name = "Test Case for provider: {0}")
@DisplayName("Retrieve a access token using authorization (GET /external/login/github) should return (200 OK)")
void testGetUserInfo() {
given().get("/external/login/github")
void testGetUserInfo(String provider) {
given().get("/external/login/" + provider)
.then()
.statusCode(Response.Status.OK.getStatusCode())
.body(
"access_token", notNullValue(),
"refresh_token", notNullValue()
);
}

static Stream<Arguments> createProviderData() {
return Stream.of(Arguments.of("github"), Arguments.of("discord"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.cloudeko.zenei.resource;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

import java.util.Map;

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);
}

@Override
public void stop() {
}

protected abstract Map<String, String> providerSpecificStubsAndConfig(WireMockServer server);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package dev.cloudeko.zenei.resource;

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 java.util.Map;

public class MockDiscordAuthorizationServerTestResource 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 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"))
.withQueryParam("client_id", WireMock.matching(".*"))
.withQueryParam("client_secret", WireMock.matching(".*"))
.withQueryParam("code", 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)
))));
} catch (Exception e) {
throw new RuntimeException(e);
}

return Map.of(
"zenei.external.auth.providers.discord.base-uri", server.baseUrl() + "/discord",
"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",
"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",
"zenei.external.auth.providers.discord.scope", "user,email"
);
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
package dev.cloudeko.zenei.resource;

import com.fasterxml.jackson.databind.ObjectMapper;
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 io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.ConfigProvider;

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

public class MockGithubAuthorizationServerTestResource implements QuarkusTestResourceLifecycleManager {

private final ObjectMapper objectMapper = new ObjectMapper();
private WireMockServer wireMockServer;
public class MockGithubAuthorizationServerTestResource extends AbstractMockAuthorizationServerTestResource {

@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer();
wireMockServer.start();

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
wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo("/login/oauth/authorize"))
server.stubFor(WireMock.get(WireMock.urlPathEqualTo("/github/login/oauth/authorize"))
.withQueryParam("client_id", WireMock.matching(".*"))
.withQueryParam("redirect_uri", WireMock.matching(".*"))
.withQueryParam("scope", WireMock.matching(".*"))
Expand All @@ -37,7 +29,7 @@ public Map<String, String> start() {
"http://localhost:" + testPort + "/external/callback/github?code=mock_code&state=mock_state")));

// Mock the access token endpoint
wireMockServer.stubFor(WireMock.post(WireMock.urlPathEqualTo("/login/oauth/access_token"))
server.stubFor(WireMock.post(WireMock.urlPathEqualTo("/github/login/oauth/access_token"))
.withQueryParam("client_id", WireMock.matching(".*"))
.withQueryParam("client_secret", WireMock.matching(".*"))
.withQueryParam("code", WireMock.matching(".*"))
Expand All @@ -49,44 +41,37 @@ public Map<String, String> start() {
))));

// Mock the GitHub user data endpoints
wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo("/user"))
server.stubFor(WireMock.get(WireMock.urlPathEqualTo("/github/user"))
.withHeader("Authorization", WireMock.equalTo("Bearer mock_access_token"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", "application/json")
.withBody(objectMapper.writeValueAsString(
new GithubUser(12345L, "testuser", "Test User", "https://example.com/avatar.jpg",
"testuser@example.com")
new GithubUser(12345L, "github-user", "Github Test User", "https://example.com/avatar.jpg",
"test@github.com")
))));

wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo("/user/emails"))
server.stubFor(WireMock.get(WireMock.urlPathEqualTo("/github/user/emails"))
.withHeader("Authorization", WireMock.equalTo("Bearer mock_access_token"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", "application/json")
.withBody(objectMapper.writeValueAsString(
List.of(
new GithubUserEmail("testuser@example.com", true, true),
new GithubUserEmail("secondary@example.com", false, true)
new GithubUserEmail("test@github.com", true, true),
new GithubUserEmail("secondary@github.com", false, true)
)
))));
} catch (Exception e) {
throw new RuntimeException(e);
}

return Map.of(
"zenei.external.auth.providers.github.base-uri", wireMockServer.baseUrl(),
"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", wireMockServer.baseUrl() + "/login/oauth/authorize",
"zenei.external.auth.providers.github.token-uri", wireMockServer.baseUrl() + "/login/oauth/access_token",
"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.scope", "user,email"
);
}

@Override
public void stop() {
if (null != wireMockServer) {
wireMockServer.stop();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dev.cloudeko.zenei.resource;

import com.github.tomakehurst.wiremock.WireMockServer;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import lombok.Getter;

import java.util.Map;

public class MockServerResource implements QuarkusTestResourceLifecycleManager {

@Getter
private static WireMockServer wireMockServer;

@Override
public Map<String, String> start() {
if (wireMockServer == null) {
wireMockServer = new WireMockServer();
wireMockServer.start();
}

return Map.of();
}

@Override
public void stop() {
if (wireMockServer != null) {
wireMockServer.stop();
wireMockServer = null;
}
}
}
8 changes: 0 additions & 8 deletions extensions/external-authentication/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-jackson-deployment</artifactId>
Expand Down
8 changes: 0 additions & 8 deletions extensions/external-authentication/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.cloudeko.zenei.extension.external.endpoint;

public class DiscordProviderEndpoints implements DefaultProviderEndpoints {

@Override
public String getAuthorizationEndpoint() {
return "https://discord.com/api/oauth2/authorize";
}

@Override
public String getTokenEndpoint() {
return "https://discord.com/api/oauth2/token";
}

@Override
public String getBaseEndpoint() {
return "https://discord.com/api";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

public class ProviderEndpoints {
public static final DefaultProviderEndpoints GITHUB = new GithubProviderEndpoints();
public static final DefaultProviderEndpoints DISCORD = new DiscordProviderEndpoints();
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ private ExternalAuthProvider createAuthProvider(String providerName, ExternalAut
yield new GithubExternalAuthProvider(providerConfig);
}

case "discord": {
yield new DiscordExternalAuthProvider(providerConfig);
}

default: {
yield null;
}
Expand Down
Loading

0 comments on commit 246dab8

Please sign in to comment.