diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a5b30f0e9..ea7edb604 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,4 +1,4 @@ -name: JavaScript Integration Tests +name: Integration Tests on: workflow_dispatch: @@ -31,3 +31,40 @@ jobs: ORY_SDK_URL: ${{ secrets.ORY_SDK_URL }} TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }} TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} + + test-java-integration: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: "17" + distribution: "temurin" + cache: "maven" + - name: Install test app dependencies + run: | + cd tests/jest/apps/identity-get-started-java + mvn dependency:resolve + - name: Start test server + run: | + cd tests/jest/apps/identity-get-started-java + mvn spring-boot:run & + env: + ORY_SDK_URL: ${{ secrets.ORY_SDK_URL }} + EXAMPLES_TEST_APP_PORT: "3000" + - name: Wait for server to be ready + run: | + timeout 30 bash -c 'until curl -f http://127.0.0.1:3000/configure; do sleep 1; done' + - name: Run Java integration tests + run: | + cd tests/jest/apps/identity-get-started-java + mvn test + env: + ORY_SDK_URL: ${{ secrets.ORY_SDK_URL }} + TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }} + TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} + - name: Stop test server + if: always() + run: | + pkill -f "spring-boot:run" || true diff --git a/docs/identities/get-started/_common/code-examples/java/LoginHandler.java b/docs/identities/get-started/_common/code-examples/java/LoginHandler.java new file mode 100644 index 000000000..bd077d867 --- /dev/null +++ b/docs/identities/get-started/_common/code-examples/java/LoginHandler.java @@ -0,0 +1,41 @@ +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import sh.ory.ApiException; +import sh.ory.api.FrontendApi; +import sh.ory.model.Session; + +import java.io.IOException; + +@RestController +public class LoginHandler { + private final FrontendApi ory; + private final String baseUrl; + + public LoginHandler(FrontendApi ory, String baseUrl) { + this.ory = ory; + this.baseUrl = baseUrl; + } + + @GetMapping("/login") + public void login(HttpServletRequest request, HttpServletResponse response) throws IOException { + String cookieHeader = request.getHeader("Cookie"); + + try { + Session session = ory.toSession(null, cookieHeader, null); + if (session != null && session.getActive() != null && session.getActive()) { + // Session is valid, return session data as JSON + response.setContentType("application/json"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write(session.toString()); + return; + } + } catch (ApiException e) { + // Session is invalid or doesn't exist + } + + // Redirect to login page + response.sendRedirect(baseUrl + "/self-service/login/browser"); + } +} diff --git a/docs/identities/get-started/_common/code-examples/java/LogoutHandler.java b/docs/identities/get-started/_common/code-examples/java/LogoutHandler.java new file mode 100644 index 000000000..505fcc282 --- /dev/null +++ b/docs/identities/get-started/_common/code-examples/java/LogoutHandler.java @@ -0,0 +1,37 @@ +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import sh.ory.ApiException; +import sh.ory.api.FrontendApi; +import sh.ory.model.LogoutFlow; + +import java.io.IOException; + +@RestController +public class LogoutHandler { + private final FrontendApi ory; + + public LogoutHandler(FrontendApi ory) { + this.ory = ory; + } + + @GetMapping("/logout") + public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException { + String cookieHeader = request.getHeader("Cookie"); + + try { + LogoutFlow logoutFlow = ory.createBrowserLogoutFlow(cookieHeader); + String logoutUrl = logoutFlow.getLogoutUrl(); + if (logoutUrl != null) { + response.sendRedirect(logoutUrl); + return; + } + } catch (ApiException e) { + // Error creating logout flow + } + + // Redirect to home page if there's an error + response.sendRedirect("/"); + } +} diff --git a/docs/identities/get-started/_common/code-examples/java/SetupDev.java b/docs/identities/get-started/_common/code-examples/java/SetupDev.java new file mode 100644 index 000000000..c2096583f --- /dev/null +++ b/docs/identities/get-started/_common/code-examples/java/SetupDev.java @@ -0,0 +1,15 @@ +import sh.ory.ApiClient; +import sh.ory.Configuration; +import sh.ory.api.FrontendApi; + +public class SetupDev { + public static final String baseUrl = System.getenv("ORY_SDK_URL") != null + ? System.getenv("ORY_SDK_URL") + : "http://localhost:4000"; + + public static FrontendApi createOryClient() { + ApiClient apiClient = Configuration.getDefaultApiClient(); + apiClient.setBasePath(baseUrl); + return new FrontendApi(apiClient); + } +} diff --git a/docs/identities/get-started/_common/code-examples/java/SignUpHandler.java b/docs/identities/get-started/_common/code-examples/java/SignUpHandler.java new file mode 100644 index 000000000..543dcda44 --- /dev/null +++ b/docs/identities/get-started/_common/code-examples/java/SignUpHandler.java @@ -0,0 +1,41 @@ +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import sh.ory.ApiException; +import sh.ory.api.FrontendApi; +import sh.ory.model.Session; + +import java.io.IOException; + +@RestController +public class SignUpHandler { + private final FrontendApi ory; + private final String baseUrl; + + public SignUpHandler(FrontendApi ory, String baseUrl) { + this.ory = ory; + this.baseUrl = baseUrl; + } + + @GetMapping("/signup") + public void signUp(HttpServletRequest request, HttpServletResponse response) throws IOException { + String cookieHeader = request.getHeader("Cookie"); + + try { + Session session = ory.toSession(null, cookieHeader, null); + if (session != null && session.getActive() != null && session.getActive()) { + // Session is valid, return session data as JSON + response.setContentType("application/json"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write(session.toString()); + return; + } + } catch (ApiException e) { + // Session is invalid or doesn't exist + } + + // Redirect to registration page + response.sendRedirect(baseUrl + "/self-service/registration/browser"); + } +} diff --git a/docs/identities/get-started/_common/code-examples/java/session/RefreshSessionHandler.java b/docs/identities/get-started/_common/code-examples/java/session/RefreshSessionHandler.java new file mode 100644 index 000000000..c9d505abb --- /dev/null +++ b/docs/identities/get-started/_common/code-examples/java/session/RefreshSessionHandler.java @@ -0,0 +1,22 @@ +package session; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +@RestController +public class RefreshSessionHandler { + private final String baseUrl; + + public RefreshSessionHandler(String baseUrl) { + this.baseUrl = baseUrl; + } + + @GetMapping("/refresh-session") + public void refreshSession(HttpServletResponse response) throws IOException { + // Redirect to login with refresh=true parameter + response.sendRedirect(baseUrl + "/ui/login?refresh=true"); + } +} diff --git a/docs/identities/get-started/_common/code-examples/java/session/RequireAuth.java b/docs/identities/get-started/_common/code-examples/java/session/RequireAuth.java new file mode 100644 index 000000000..c8bea71d9 --- /dev/null +++ b/docs/identities/get-started/_common/code-examples/java/session/RequireAuth.java @@ -0,0 +1,40 @@ +package session; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerInterceptor; +import sh.ory.ApiException; +import sh.ory.api.FrontendApi; +import sh.ory.model.Session; + +import java.io.IOException; + +public class RequireAuth implements HandlerInterceptor { + private final FrontendApi ory; + private final String baseUrl; + + public RequireAuth(FrontendApi ory, String baseUrl) { + this.ory = ory; + this.baseUrl = baseUrl; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + String cookieHeader = request.getHeader("Cookie"); + + try { + Session session = ory.toSession(null, cookieHeader, null); + if (session != null && session.getActive() != null && session.getActive()) { + // Store session in request attribute + request.setAttribute("session", session); + return true; + } + } catch (ApiException e) { + // Session is invalid or doesn't exist + } + + // Redirect to login page + response.sendRedirect(baseUrl + "/self-service/login/browser"); + return false; + } +} diff --git a/docs/identities/get-started/_common/code-examples/java/session/SessionHandler.java b/docs/identities/get-started/_common/code-examples/java/session/SessionHandler.java new file mode 100644 index 000000000..73c04f9e0 --- /dev/null +++ b/docs/identities/get-started/_common/code-examples/java/session/SessionHandler.java @@ -0,0 +1,19 @@ +package session; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import sh.ory.model.Session; + +@RestController +public class SessionHandler { + @GetMapping("/session") + public ResponseEntity getSession(HttpServletRequest request) { + Session session = (Session) request.getAttribute("session"); + if (session != null && session.getIdentity() != null && session.getIdentity().getTraits() != null) { + return ResponseEntity.ok(session.getIdentity().getTraits()); + } + return ResponseEntity.notFound().build(); + } +} diff --git a/docs/identities/get-started/session-management.mdx b/docs/identities/get-started/session-management.mdx index d3df8c74c..6d4f304e3 100644 --- a/docs/identities/get-started/session-management.mdx +++ b/docs/identities/get-started/session-management.mdx @@ -15,6 +15,9 @@ import nextRefreshSession from '!!raw-loader!./_common/code-examples/nextjs/sess import nextCheckSession from '!!raw-loader!./_common/code-examples/nextjs/session/middleware.ts' import goCheckSession from '!!raw-loader!./_common/code-examples/go/middleware.go' import goRefreshHandler from '!!raw-loader!./_common/code-examples/go/refresh_handler.go' +import javaRequireAuth from '!!raw-loader!./_common/code-examples/java/session/RequireAuth.java' +import javaSessionHandler from '!!raw-loader!./_common/code-examples/java/session/SessionHandler.java' +import javaRefreshSession from '!!raw-loader!./_common/code-examples/java/session/RefreshSessionHandler.java' ``` After a user has logged in, Ory creates a session cookie that your application can use to verify the user's authentication status. @@ -51,6 +54,19 @@ You can protect routes by checking for a session cookie. {goCheckSession} ``` +```mdx-code-block + + +``` + +```mdx-code-block +{javaRequireAuth} +``` + +```mdx-code-block +{javaSessionHandler} +``` + ```mdx-code-block @@ -87,6 +103,15 @@ You can refresh user sessions to extend their expiration time: {goRefreshHandler} ``` +```mdx-code-block + + +``` + +```mdx-code-block +{javaRefreshSession} +``` + ```mdx-code-block diff --git a/docs/identities/get-started/sign-in.mdx b/docs/identities/get-started/sign-in.mdx index 347784765..6e9951768 100644 --- a/docs/identities/get-started/sign-in.mdx +++ b/docs/identities/get-started/sign-in.mdx @@ -15,6 +15,7 @@ import { FrameworkCodeTabs } from '@site/src/components/GuidesComponents' import jsLogin from '!!raw-loader!./_common/code-examples/js/login.js' import goLogin from '!!raw-loader!./_common/code-examples/go/login_handler.go' import nextjsLogin from '!!raw-loader!./_common/code-examples/nextjs/login.tsx' +import javaLogin from '!!raw-loader!./_common/code-examples/java/LoginHandler.java' ``` The sign in flow follows the same pattern as the [sign up flow](/docs/identities/get-started/sign-up) but instead of redirecting @@ -48,6 +49,15 @@ to the registration page, it redirects to the login page. ``` +```mdx-code-block + + +``` + +```mdx-code-block +{javaLogin} +``` + ```mdx-code-block diff --git a/docs/identities/get-started/sign-out.mdx b/docs/identities/get-started/sign-out.mdx index 513b101c9..3bb41a31d 100644 --- a/docs/identities/get-started/sign-out.mdx +++ b/docs/identities/get-started/sign-out.mdx @@ -15,6 +15,7 @@ import { FrameworkCodeTabs, ImplementationSteps } from '@site/src/components/Gui import jsLogout from '!!raw-loader!./_common/code-examples/js/logout.js' import nextLogout from '!!raw-loader!./_common/code-examples/nextjs/logout.tsx' import goLogout from '!!raw-loader!./_common/code-examples/go/logout_handler.go' +import javaLogout from '!!raw-loader!./_common/code-examples/java/LogoutHandler.java' ``` The logout flow allows users to securely terminate their sessions. This guide shows how to implement proper logout functionality @@ -43,6 +44,13 @@ in your application. {goLogout} +```mdx-code-block + + +``` + +{javaLogout} + ```mdx-code-block diff --git a/docs/identities/get-started/sign-up.mdx b/docs/identities/get-started/sign-up.mdx index 1edad1be7..60b71e610 100644 --- a/docs/identities/get-started/sign-up.mdx +++ b/docs/identities/get-started/sign-up.mdx @@ -15,6 +15,7 @@ import { FrameworkCodeTabs } from '@site/src/components/GuidesComponents' import jsSignUp from '!!raw-loader!./_common/code-examples/js/sign-up.js' import goSignUp from '!!raw-loader!./_common/code-examples/go/signup_handler.go' import nextSignUp from '!!raw-loader!./_common/code-examples/nextjs/sign-up.tsx' +import javaSignUp from '!!raw-loader!./_common/code-examples/java/SignUpHandler.java' ``` This guide shows how to implement a secure sign up flow that authenticates users and creates sessions. @@ -56,6 +57,15 @@ the Ory registration page. ``` +```mdx-code-block + + +``` + +```mdx-code-block +{javaSignUp} +``` + ```mdx-code-block diff --git a/src/components/GuidesComponents/FrameworkCodeTabs.tsx b/src/components/GuidesComponents/FrameworkCodeTabs.tsx index 92d98b60b..d1ae4c38c 100644 --- a/src/components/GuidesComponents/FrameworkCodeTabs.tsx +++ b/src/components/GuidesComponents/FrameworkCodeTabs.tsx @@ -6,6 +6,7 @@ const DEFAULT_FRAMEWORKS = [ { value: "javascript", label: "Express (JS)" }, { value: "nextjs", label: "Next.js" }, { value: "go", label: "Go" }, + { value: "java", label: "Java" }, ] export const FrameworkCodeTabs = ({ children }) => { diff --git a/tests/jest/apps/identity-get-started-java/pom.xml b/tests/jest/apps/identity-get-started-java/pom.xml new file mode 100644 index 000000000..5ed436ddd --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.ory + ory-identity-get-started-java-tests-app + 1.0.0 + Ory Identity Get Started Java Test App + Test server for Ory Identity get started Java examples + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + sh.ory + ory-client + 1.22.11 + + + org.springframework.boot + spring-boot-starter-test + test + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/../../../../docs/identities/get-started/_common/code-examples/java + + + + + + + + diff --git a/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/AppConfig.java b/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/AppConfig.java new file mode 100644 index 000000000..a8e90013a --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/AppConfig.java @@ -0,0 +1,64 @@ +package com.ory.test; + +import session.RequireAuth; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import sh.ory.ApiClient; +import sh.ory.Configuration; +import sh.ory.api.FrontendApi; + +@Configuration +public class AppConfig implements WebMvcConfigurer { + @Value("${ory.sdk.url:http://localhost:4000}") + private String baseUrl; + + @Autowired + private RequireAuth requireAuth; + + @Bean + public FrontendApi frontendApi() { + ApiClient apiClient = Configuration.getDefaultApiClient(); + apiClient.setBasePath(baseUrl); + return new FrontendApi(apiClient); + } + + @Bean + public SignUpHandler signUpHandler(FrontendApi frontendApi) { + return new SignUpHandler(frontendApi, baseUrl); + } + + @Bean + public LoginHandler loginHandler(FrontendApi frontendApi) { + return new LoginHandler(frontendApi, baseUrl); + } + + @Bean + public LogoutHandler logoutHandler(FrontendApi frontendApi) { + return new LogoutHandler(frontendApi); + } + + @Bean + public session.SessionHandler sessionHandler() { + return new session.SessionHandler(); + } + + @Bean + public session.RefreshSessionHandler refreshSessionHandler() { + return new session.RefreshSessionHandler(baseUrl); + } + + @Bean + public RequireAuth requireAuth(FrontendApi frontendApi) { + return new RequireAuth(frontendApi, baseUrl); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(requireAuth) + .addPathPatterns("/session"); + } +} diff --git a/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/Application.java b/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/Application.java new file mode 100644 index 000000000..cadadba4a --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/Application.java @@ -0,0 +1,13 @@ +package com.ory.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan(basePackages = {"com.ory.test", "", "session"}) +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/ConfigureController.java b/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/ConfigureController.java new file mode 100644 index 000000000..1ba740be9 --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/src/main/java/com/ory/test/ConfigureController.java @@ -0,0 +1,18 @@ +package com.ory.test; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +public class ConfigureController { + @Value("${ory.sdk.url:http://localhost:4000}") + private String orySdkUrl; + + @GetMapping("/configure") + public Map configure() { + return Map.of("orySdkUrl", orySdkUrl); + } +} diff --git a/tests/jest/apps/identity-get-started-java/src/main/resources/application.yaml b/tests/jest/apps/identity-get-started-java/src/main/resources/application.yaml new file mode 100644 index 000000000..c1588a571 --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +server: + port: ${EXAMPLES_TEST_APP_PORT:3000} + +ory: + sdk: + url: ${ORY_SDK_URL:http://localhost:4000} diff --git a/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentityLoginTest.java b/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentityLoginTest.java new file mode 100644 index 000000000..dd4f0b916 --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentityLoginTest.java @@ -0,0 +1,36 @@ +package com.ory.test; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +public class IdentityLoginTest { + private static final String BASE_URL = TestServerUtils.getBaseUrl(); + private static final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .followRedirects(HttpClient.Redirect.NEVER) + .build(); + + @Test + public void testRedirectsToOryLoginUIWhenNoSessionOnLogin() throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/login")) + .timeout(Duration.ofSeconds(10)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.discarding()); + + assertEquals(302, response.statusCode(), "Expected 302 redirect status"); + String location = response.headers().firstValue("location") + .orElseThrow(() -> new AssertionError("No location header in redirect")); + assertTrue(location.contains("/self-service/login/browser"), + "Location should contain /self-service/login/browser, got: " + location); + } +} diff --git a/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentityLogoutTest.java b/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentityLogoutTest.java new file mode 100644 index 000000000..14457fb3e --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentityLogoutTest.java @@ -0,0 +1,64 @@ +package com.ory.test; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class IdentityLogoutTest { + private static final String BASE_URL = TestServerUtils.getBaseUrl(); + private static final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .followRedirects(HttpClient.Redirect.NEVER) + .build(); + + private static String testEmail; + private static String testPassword; + private static String oryBaseUrl; + + @BeforeAll + public static void setUp() { + testEmail = System.getenv("TEST_USER_EMAIL"); + String testPasswordEnv = System.getenv("TEST_USER_PASSWORD"); + oryBaseUrl = System.getenv("ORY_SDK_URL"); + + assumeTrue(testEmail != null && !testEmail.isEmpty(), + "TEST_USER_EMAIL environment variable must be set"); + assumeTrue(testPasswordEnv != null && !testPasswordEnv.isEmpty(), + "TEST_USER_PASSWORD environment variable must be set"); + assumeTrue(oryBaseUrl != null && !oryBaseUrl.isEmpty(), + "ORY_SDK_URL environment variable must be set"); + + testPassword = testPasswordEnv; + } + + @Test + public void testLogsInUsingPredefinedEnvVariablesThenLogsOut() throws IOException, InterruptedException { + // Login and get session cookie + String sessionCookie = TestServerUtils.selfServiceLogin(oryBaseUrl, testEmail, testPassword); + + // Logout + HttpRequest logoutRequest = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/logout")) + .header("Cookie", sessionCookie) + .timeout(Duration.ofSeconds(10)) + .build(); + + HttpResponse logoutResponse = httpClient.send(logoutRequest, HttpResponse.BodyHandlers.discarding()); + + // After logout, user should be redirected to home page + assertEquals(302, logoutResponse.statusCode(), "Expected 302 redirect status"); + String logoutLocation = logoutResponse.headers().firstValue("location") + .orElseThrow(() -> new AssertionError("No location header in logout response")); + assertEquals("http://127.0.0.1:3000/", logoutLocation, + "After logout, should redirect to home page"); + } +} diff --git a/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentitySignupTest.java b/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentitySignupTest.java new file mode 100644 index 000000000..dd8a9be31 --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/IdentitySignupTest.java @@ -0,0 +1,36 @@ +package com.ory.test; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +public class IdentitySignupTest { + private static final String BASE_URL = TestServerUtils.getBaseUrl(); + private static final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .followRedirects(HttpClient.Redirect.NEVER) + .build(); + + @Test + public void testRedirectsToOryRegistrationUIWhenNoSessionOnSignup() throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/signup")) + .timeout(Duration.ofSeconds(10)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.discarding()); + + assertEquals(302, response.statusCode(), "Expected 302 redirect status"); + String location = response.headers().firstValue("location") + .orElseThrow(() -> new AssertionError("No location header in redirect")); + assertTrue(location.contains("/self-service/registration/browser"), + "Location should contain /self-service/registration/browser, got: " + location); + } +} diff --git a/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/TestServerUtils.java b/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/TestServerUtils.java new file mode 100644 index 000000000..cc0c51bfa --- /dev/null +++ b/tests/jest/apps/identity-get-started-java/src/test/java/com/ory/test/TestServerUtils.java @@ -0,0 +1,133 @@ +package com.ory.test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TestServerUtils { + private static final String BASE_URL = "http://127.0.0.1:3000"; + private static final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static String getBaseUrl() { + return BASE_URL; + } + + public static void waitForServer() throws InterruptedException, IOException { + int maxAttempts = 30; + int attempt = 0; + + while (attempt < maxAttempts) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/configure")) + .timeout(Duration.ofSeconds(2)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return; + } + } catch (Exception e) { + // Server not ready yet + } + + attempt++; + Thread.sleep(1000); + } + + throw new RuntimeException("Server failed to start within timeout"); + } + + public static String selfServiceLogin(String baseUrl, String email, String password) throws IOException, InterruptedException { + // Get login API flow + HttpRequest loginFlowRequest = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/self-service/login/api")) + .header("Accept", "application/json") + .build(); + + HttpResponse loginFlowResponse = httpClient.send(loginFlowRequest, HttpResponse.BodyHandlers.ofString()); + JsonNode loginFlowJson = objectMapper.readTree(loginFlowResponse.body()); + + // Extract flow ID and CSRF token + String flowId = loginFlowJson.get("id").asText(); + String csrfToken = extractCsrfToken(loginFlowJson); + + // Submit login + String loginBody = String.format( + "{\"method\":\"password\",\"csrf_token\":\"%s\",\"identifier\":\"%s\",\"password\":\"%s\"}", + csrfToken, email, password + ); + + HttpRequest loginSubmitRequest = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/self-service/login?flow=" + flowId)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(loginBody)) + .build(); + + HttpResponse loginSubmitResponse = httpClient.send(loginSubmitRequest, HttpResponse.BodyHandlers.ofString()); + + if (loginSubmitResponse.statusCode() != 200) { + throw new RuntimeException("Login failed with status " + loginSubmitResponse.statusCode()); + } + + // Extract session cookie from response + String setCookieHeader = loginSubmitResponse.headers().firstValue("set-cookie") + .orElseThrow(() -> new RuntimeException("No set-cookie header in login response")); + + return parseCookie(setCookieHeader); + } + + private static String extractCsrfToken(JsonNode json) { + JsonNode ui = json.get("ui"); + if (ui != null) { + JsonNode nodes = ui.get("nodes"); + if (nodes != null && nodes.isArray()) { + for (JsonNode node : nodes) { + JsonNode attributes = node.get("attributes"); + if (attributes != null && "csrf_token".equals(attributes.get("name").asText())) { + JsonNode value = attributes.get("value"); + if (value != null) { + return value.asText(); + } + } + } + } + } + return ""; + } + + private static String parseCookie(String setCookieHeader) { + // Extract the first cookie value (session cookie) + // Handle multiple cookies separated by comma (RFC 6265) + String[] cookieStrings = setCookieHeader.split(", "); + for (String cookieStr : cookieStrings) { + cookieStr = cookieStr.trim(); + String[] parts = cookieStr.split(";"); + if (parts.length > 0 && parts[0].contains("=")) { + String nameValue = parts[0].trim(); + // Check if it's a session cookie (starts with ory_session) + if (nameValue.startsWith("ory_session")) { + return nameValue; + } + } + } + // Fallback: return first cookie if no session cookie found + String[] parts = setCookieHeader.split(";"); + if (parts.length > 0) { + return parts[0].trim(); + } + throw new RuntimeException("Could not parse cookie from header: " + setCookieHeader); + } +}