diff --git a/README.md b/README.md index 1b9c0db..4333cbe 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,83 @@ # Ory Hydra Reference Implementation - Java -This is an _unofficial_ reference implementation of the User Login and Consent flow of an -[Ory Hydra](https://github.com/ory) OAuth 2.0 server written in Java with SpringBoot. This project demos some -key features, flows, and integrations of the OAuth 2.0 Authorization Code Grant flow. It is meant to be a foundation -for production implementations, but it is not an exhaustive implementation nor is it guaranteed to be secure, bug -free, fully tested, or production ready. +[![build](https://github.com/ardetrick/ory-hydra-refrence-java/actions/workflows/gradle.yml/badge.svg)](https://github.com/ardetrick/ory-hydra-refrence-java/actions/workflows/gradle.yml) +![Java 21](https://img.shields.io/badge/Java-21-orange) -Similar reference implementations can be found on -the [Getting Started](https://www.ory.sh/docs/getting-started/overview) -page of the official Ory website. +An _unofficial_ Java reference implementation of the login, consent, and logout app that an +[Ory Hydra](https://github.com/ory) OAuth 2.0 server needs. -## Introduction +Ory Hydra is an OpenID Certified OAuth 2.0 and OpenID Connect server, but it is not an identity +provider — it delegates user login, consent, and logout to an app like this one. This project is +that app, built with Spring Boot and meant as a foundation to copy from, including its +[test setup](#testing), which runs against a real Hydra instead of mocks. It is not exhaustive, and +it is not guaranteed to be secure, bug free, fully tested, or production ready. -Ory Hydra is an open source project OpenID Certified OAuth 2.0 Server and OpenID Connect Provider. Ory Hydra is not an -identity provider (user sign up, user login, password reset flow), but connects to your existing identity provider -through a login and consent app. This project is an example of such a login and consent app. It aims to be a useful -reference for other Java implementations, providing insight into not only how to integrate with Ory Hydra but also how -to effectively test the entirety of the system using SpringBootTest, Test Containers, and Playwright. +For other languages and frameworks, Ory lists reference implementations on its +[Getting Started](https://www.ory.sh/docs/getting-started/overview) page. Comparable products +include +[Spring Authorization Server](https://spring.io/projects/spring-authorization-server), +[Auth0](https://auth0.com/docs/authenticate/protocols/oauth), [Keycloak](https://www.keycloak.org/), +[Amazon Cognito](https://docs.aws.amazon.com/cognito/index.html), and [Dex](https://dexidp.io/). -The following features have been implemented: +Whatever you choose, do not write the oauth endpoints yourself! -- [Login](https://www.ory.sh/docs/hydra/guides/login) -- [Consent](https://www.ory.sh/docs/hydra/guides/consent) -- [OIDC](https://www.ory.sh/docs/hydra/concepts/openid-connect-oidc) (with custom claims) +## Example flow -Similar products include but are not limited to: +The app implements the OAuth 2.0 authorization code flow: log in, authorize the requested scopes, +and the app exchanges the authorization code for real tokens from Hydra. -- [Spring Authorization Server](https://spring.io/projects/spring-authorization-server) -- [Auth0](https://auth0.com/docs/authenticate/protocols/oauth) -- [Keycloak](https://www.keycloak.org/) -- [Amazon Cognito](https://docs.aws.amazon.com/cognito/index.html) -- [Dex](https://dexidp.io/) +![consent screen](docs/images/authorization-code-flow/2-consent.png) -Whatever you choose, do not write the oauth endpoints yourself! +
+See the full flow, step by step -## Prerequisites: What do you need to get started? +Log in — the demo credentials are pre-filled. -- Java 21 - - required by the SpringBoot plugin at build time - - Gradle tool chain is used to compile and run tests -- Docker (required for running tests and for `./gradlew bootTestRun`) -- jq (used for some demos, not a hard dependency) +![login screen](docs/images/authorization-code-flow/1-login.png) -## Technologies Used +Authorize the scopes the application is requesting. -- Java 21 -- SpringBoot -- Gradle -- Test Containers -- Ory Hydra -- Docker -- Freemarker -- Lombok -- GitHub Actions +![consent screen](docs/images/authorization-code-flow/2-consent.png) + +Hydra redirects back with an authorization code, which the app exchanges for tokens. + +![callback screen](docs/images/authorization-code-flow/3-callback-page.png) + +The result is a real access token, ID token, and refresh token from Hydra. + +![token response](docs/images/authorization-code-flow/4-tokens.png) + +
+ +The app also implements Hydra's logout flow, and the remember-me options that let Hydra skip the +login and consent screens on a return visit. ## Running -### Running Locally +Requirements: -One command starts the app together with a real Ory Hydra container and a pre-registered demo -client (Docker must be running): +- Java 21 +- Docker, running ``` ./gradlew bootTestRun ``` -Then open http://localhost:8080 and click "Start the OAuth flow". The credentials are -`foo@bar.com` / `password`; the flow ends on a page that exchanges the authorization code for -real tokens. Hydra runs on its default ports (public 4444, admin 4445), the same -single-container setup the functional tests use via -[testcontainers-ory-hydra](https://github.com/ardetrick/testcontainers-ory-hydra) — see -`TestOryHydraReferenceApplication` for how the container is configured and the demo client is -seeded. Ports 4444, 4445, and 8080 must be free. +This starts the app together with a real Ory Hydra container and seeds a demo client. Open +http://localhost:8080, click "Start the OAuth flow", and log in with `foo@bar.com` / `password`; +the flow ends on a page that exchanges the authorization code for real tokens. Hydra uses its +default ports (4444 public, 4445 admin), so 4444, 4445, and 8080 must be free. See +`TestOryHydraReferenceApplication` for how the container and demo client are configured. -### Running With Your Own Ory Hydra +### Running against your own Ory Hydra To run the app against a Hydra you manage yourself, configure your Hydra with the app's endpoints as the login, consent, and logout URLs (`URLS_LOGIN=http://localhost:8080/login`, and likewise `/consent` and `/logout`), then start the app with `./gradlew bootRun`. If your Hydra's admin API is not at the default `http://localhost:4445`, point the app at it with the -`reference-app.hydra.base-path` property. This path starts with whatever OAuth2 clients your -Hydra already has — the landing page at http://localhost:8080 lists them and includes -instructions for creating one. To see each step of the flow and token exchange as raw terminal -commands, see +`reference-app.hydra.base-path` property. The landing page at http://localhost:8080 lists whatever +OAuth2 clients your Hydra already has and includes instructions for creating one. To see each step +of the flow and token exchange as raw terminal commands, see [walking the authorization code flow by hand](docs/manual-token-exchange.md).
@@ -96,131 +91,86 @@ docker run --rm --name hydra \ -e URLS_LOGIN=http://localhost:8080/login \ -e URLS_CONSENT=http://localhost:8080/consent \ -e URLS_LOGOUT=http://localhost:8080/logout \ - --entrypoint sh oryd/hydra:v25.4.0 \ + --entrypoint sh oryd/hydra:v26.2.0 \ -c "hydra migrate sql -e --yes && hydra serve all --dev" ```
-### Running Functional Tests +## Testing -The functional tests for this project run along all other tests with the standard gradle command. +The functional tests are the part most worth copying. Instead of mocking Hydra, they drive every +flow through the real browser UI against a real Ory Hydra running in a container — the same setup +`bootTestRun` uses. It is more work to wire up than mocks, but it catches the integration failures +mocks paper over, and it gives you a ready-made pattern for testing your own Hydra integration. + +Run them with everything else: ``` ./gradlew test ``` -The functional tests are unique because there is practically no mocking. This makes for a slightly more complicated -setup, but it allows us to reproduce scenarios in a context very similar to what would be seen in production, all the -way from interacting with the UI back to Ory Hydra. -
How the test rig works -1. Using `@SpringBootTest`, the application is started on a random port. Note that the application also configures two - extra controllers to help facilitate testing. +1. Using `@SpringBootTest`, the application is started on a random port. Note that the application + also configures two extra controllers to help facilitate testing. 2. A Playwright browser instance is created (shared by all tests). -3. A single Test Container instance of Ory Hydra is started and shared by every test in the class. It runs with an - in-container SQLite database. -4. Before each test, a unique Hydra OAuth client is created. Hydra remembers consent per subject and client, so a - fresh client per test keeps tests isolated on the shared container. +3. A single Test Container instance of Ory Hydra is started and shared by every test in the class. It + runs with an in-container SQLite database. +4. Before each test, a unique Hydra OAuth client is created. Hydra remembers consent per subject and + client, so a fresh client per test keeps tests isolated on the shared container. 5. The Playwright browser loads the `/oauth2/auth` endpoint with the client's information. 6. The Playwright api is used to interact with the UI just as a user would do. 7. Optionally, the code may be exchanged for the token response. -The extra controllers created are not ideal but are useful for testing. One of them is a `ForwardingController` which -helps work around some networking challenges with a circular dependency in configuration between the application and Ory -Hydra. At start up, the application must be aware of the urls of Hydra and Hydra must be aware of the urls of -the application. In production, this would not be an issue because static urls should be used. But in a test context -both -the application and Hydra are running on dynamic ports. The second controller is `ClientCallBackController` which -provides a hook for the client call back. This allows us to verify that Hydra actually calls the client's callback url -and provides us access to the `code` value so that it can be exchanged for the token response. +The extra controllers created are not ideal but are useful for testing. One of them is a +`ForwardingController` which helps work around some networking challenges with a circular dependency +in configuration between the application and Ory Hydra. At start up, the application must be aware of +the urls of Hydra and Hydra must be aware of the urls of the application. In production, this would +not be an issue because static urls should be used. But in a test context both the application and +Hydra are running on dynamic ports. The second controller is `ClientCallBackController` which +provides a hook for the client call back. This allows us to verify that Hydra actually calls the +client's callback url and provides us access to the `code` value so that it can be exchanged for the +token response. -Since the token flow of OAuth is inherently UI driven, it is imperative that the UI be the driver for the tests. To aid -with this the `Playwright` framework is used. It allows us to use a headless driver to load the UI and use HTML -selectors to interact with the loaded page just like a human would. +Since the token flow of OAuth is inherently UI driven, it is imperative that the UI be the driver for +the tests. To aid with this the `Playwright` framework is used. It allows us to use a headless driver +to load the UI and use HTML selectors to interact with the loaded page just like a human would.
-## OAuth 2.0 Authorization Code Grant Flow - -The OAuth 2.0 Authorization Code Grant flow is a common OAuth flow that allows a client to request access to a user's -resources on a resource server. In this flow, the client first redirects the user to the authorization server to -authenticate and authorize the client's access to the user's resources. If the user grants access, the authorization -server sends an authorization code to the client, which the client can then exchange for an access token. The access -token can then be used to access the user's resources on the resource server. - -```mermaid -sequenceDiagram - autonumber - participant User - participant User Agent - participant Identity Provider - participant Authorization Server - participant OAuth Client Server - User->>+User Agent: Requests /oauth2/auth endpoint. - User Agent->>+Authorization Server: Requests /oauth2/auth endpoint. - Authorization Server->>-User Agent: Return 302 to LOGIN_URL. - User Agent->>+Identity Provider: Follow redirect (302) to LOGIN_URL. - Identity Provider->>-User Agent: Return login HTML. - User Agent->>-User: Prompt for login credentials. - User->>User Agent: Enter credentials. - User->>+User Agent: Submit form. - User Agent->>+Identity Provider: Submit form. - Identity Provider->>Identity Provider: Validate credentials. - Identity Provider->>User Agent: Return 302 to auth server. - User Agent->>Authorization Server: Follow redirect for consent. - Authorization Server->>User Agent: Return 302 to CONSENT_URL. - User Agent->>Identity Provider: Follow 302 to CONSENT_URL. - Identity Provider->>User Agent: Return consent HTML. - User Agent->>User: Prompt for consent. - User->>User Agent: Submit consent form. - User Agent->>Identity Provider: Verify consent submission. - Identity Provider->>User Agent: Redirect 302 redirect to auth server. - User Agent->>Authorization Server: Follow redirect. - Authorization Server->>Authorization Server: Complete flow. - Authorization Server->>User Agent: Redirect 302 with code in query param. - User Agent->>OAuth Client Server: Send code to be exchanged. - OAuth Client Server->>Authorization Server: Exchange code (include client ID and secret). - Authorization Server->>Authorization Server: Validate exchange request. - Authorization Server->>OAuth Client Server: Return token response. - OAuth Client Server->>OAuth Client Server: Process token response. - OAuth Client Server->>User Agent: Direct browser accordingly. - User Agent->>User: User is authed! -``` +## OpenID Connect -## OpenID Connect (OIDC) +When the authorization code is exchanged, the token response includes an `id_token`: a JWT +describing the authenticated user. This app also attaches a custom claim, `exampleCustomClaimKey`, +which is not part of the OIDC spec, to show how to add your own (see `OryHydraRequestMapper`). -[OIDC](https://openid.net/connect/) is a layer on top of OAuth. This project demonstrates some basic features of OIDC. -When the code is exchanged the response contains a `id_token` key with a JWT string. Additional information about that -token can be found [here](https://openid.net/specs/openid-connect-core-1_0.html). - -This demo also shows how to include "custom" claims that are not part of the OIDC spec. -Look in the JWT for the value `example custom claim value`. - -Here is an example JWT json: +Decoded, the `id_token` from a `bootTestRun` session looks like this: ```json { - "at_hash": "mcH8FiS4zkwI-4tZZbow8w", + "at_hash": "9rz-A4HOh5aQtiQ5Rgz3AA", "aud": [ - "ac844c48-bf3d-4e81-87aa-7bcf74dc092c" + "demo-client" ], - "auth_time": 1671939732, + "auth_time": 1785070836, "exampleCustomClaimKey": "example custom claim value", - "exp": 1671943333, - "iat": 1671939733, - "iss": "http://localhost:59029/integration-test-public-proxy", - "jti": "57e6a5b7-bf6c-44c4-a8b3-5c4db55da72c", - "rat": 1671939731, - "sid": "b1ef49ac-f42b-457e-b3e1-295a91aaefc9", + "exp": 1785074436, + "iat": 1785070836, + "iss": "http://localhost:4444", + "jti": "cb8d373e-699d-4758-b0ae-fb3ef40106e4", + "rat": 1785070836, + "sid": "6dd2674b-19ee-4f24-950a-015e143ee300", "sub": "foo@bar.com" } ``` -The above JWT was signed using a private key and can be verified with this JWKS public key (as fetched from -`/.well-known/jwks.json`). Note how the response is an array but in this instance only a single key was returned. +Hydra signs the token with a rotating RSA key; the matching public keys are published at +`/.well-known/jwks.json`. Paste the `id_token` and JWKS into [jwt.io](https://jwt.io) to verify it. + +
+Example JWKS ```json { @@ -228,43 +178,32 @@ The above JWT was signed using a private key and can be verified with this JWKS { "use": "sig", "kty": "RSA", - "kid": "b6d33bf1-59d5-4abf-9646-d1f321675f2b", + "kid": "9712c2d2-741e-4b58-a70f-07c2eb3be94a", "alg": "RS256", - "n": "yFM_NznB3GdBMNJI9YGBmzGRBx3qkTzBfReOOq2DXRBNCkoZZOMSlfv-qqruo-pfbaLwPoz2pww81h9R2hcpoZUbaLb5R3rHOmIYftjiorjzjiLnFlndY5rq3foLxZxcZ6dYBDyS3qzZgf8hUs3CH__kG4MNAAD1Hoj8pER-_jFsAyVLBXpNrIy2aiuUscnFuOtK06LbfX0OjasKSKnx_IGXMje_uA2xziA5AUy5sm4wHWhcFWNCHNH5IgP-AHmg19lm7Swd_OlFfhxg43A7AfypV4-OdBb4qhReEgNr6Fnl761gELYfgDxGZXh6o3vs-V6s4g3fMGNGk4JYHFkkCvlclAg9XaprWKFbnhA8-elqqKWNIShf32uTcajf9rMWh_4M-mLiOPDTaJsZg0_f1z-MyE2_MDe-aRURYVY7tlQYF5MnY-Hg5uxn1QEghezclPe5YWUOtI39u__DgA6X1bgQlE5n4SmvyBTuFScxTEqsidwSiIY5hJh0ek60ds16V3I-XafQ9I6JmQ1TRHxdJfpAxYH5CbroSn7lRA-Tlqd-iaJ3ZHTIBuPUa2kZGwSE8zkFPyd803FhqM-cJTtoDIS0piNd8tn6d_-KJ62jWxJeyCWiOqMvTLHdobz0p9u181l2DU7qR07J1g4qtG0pPQfs6931hLZrG6gXsd2CYfs", + "n": "09Isnxv-Ce9nd9slEBCDDlwTPPuxcw1QYiJFFZsOfgYDMKOXbsP_ipHLqcpTmQAcnQxxm7cyo1tEagxCeri3erel-jjw8ZTLNhwiHq208XTUdX1T7RqdWHinKgGnv_JHMY6xKVtP8V-l1u_IfJnptFsQ2Q3oASarGqQxW8P-rYtnykrkfW8D81LVb7BEv-OXm4U3KyB7bCUqDm7TOsEUJuyD9LwT_cHL35oIKdYzuRUQi1UIjBMu4usv-2l4x8tWGfUHdQo3oKOufmo7ZbAKErIahPm2KyWUbmM4qrvd8vrMOdgPKJ3V-Z2ACOJJjIsShZWXH1aTdcK0L3w3M8wEzyh3Cz8ff5rtbcKUc54BM4aOSLTffg1geqiLCQZWB2eORSA_Q9TVKeFxX0v7kMSxbmLeMYq5kAMGhnepI451OAH9vkbJlqo32HN3fAPvYXIPCb5nVkkA9LOEfHYbaiXGfI0ZQISxNu1QbC9eC6fqECn9xBGpVHry3Rq6cNRX8Ut3N6TlLOnP9TxoYDprh98qfwrvEkcs3uVn8sjVaCJddlvUhFZWjCBhj5wHNYlcug8G-PLT_s-QKoXC9OOmnAcryl1d4jYQ6giJBCuuIHfs4iZxgdkKOWKt6PKOQHmZdU1xIlVqqVMY2LR25upaAijj5LcGQPilkdgVZPNpk3y7Qw0", "e": "AQAB" } ] } ``` -The online tool, [jwt.io](https://jwt.io), is useful for verifying tokens during testing. For example, the above jwt is -verified with the -above -jwk [like this](https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImI2ZDMzYmYxLTU5ZDUtNGFiZi05NjQ2LWQxZjMyMTY3NWYyYiIsInR5cCI6IkpXVCJ9.eyJhdF9oYXNoIjoibWNIOEZpUzR6a3dJLTR0Wlpib3c4dyIsImF1ZCI6WyJhYzg0NGM0OC1iZjNkLTRlODEtODdhYS03YmNmNzRkYzA5MmMiXSwiYXV0aF90aW1lIjoxNjcxOTM5NzMyLCJleGFtcGxlQ3VzdG9tQ2xhaW1LZXkiOiJleGFtcGxlIGN1c3RvbSBjbGFpbSB2YWx1ZSIsImV4cCI6MTY3MTk0MzMzMywiaWF0IjoxNjcxOTM5NzMzLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjU5MDI5L2ludGVncmF0aW9uLXRlc3QtcHVibGljLXByb3h5IiwianRpIjoiNTdlNmE1YjctYmY2Yy00NGM0LWE4YjMtNWM0ZGI1NWRhNzJjIiwicmF0IjoxNjcxOTM5NzMxLCJzaWQiOiJiMWVmNDlhYy1mNDJiLTQ1N2UtYjNlMS0yOTVhOTFhYWVmYzkiLCJzdWIiOiJmb29AYmFyLmNvbSJ9.ZRCh1sjNBIe8N_XZm97JeOpJN-T4zB3M8-PH_B-9k3cNLK3u9Ku0nBXTxQ99idrlrxyWQPT7qQK5dAzBis3M2SJ36sWq5BMjGi7bq_nU_Cj_1hzo1HTcdHXkh9iMH4zyBeQPz8BTVGgVE3rSd3tsCOMiwub7Wp3sQKDSHkCkSs6zZcsCCJS9AVQym4ltJyayYJ2lXUg7XMzuFWeyAhuVrusY8wXDv4NRIZOM6ymtHOTxe9UUVKgO8sCnUAVLqz_w2aQIYRy-u-BpT09uEalqTo27rKrUZhHjOd3LgGGVVSGp5_RM_wpK4zkiA0G7CEpU39t0ib6Ix7VWxquGWKaLIA2YqKn3ZGkTdGlByPx5N8ubPtaOxzmlgafbNTjmXxBMXU03hUXCiSaZefBZFB9jiE92-6ZKKvAFqAoaMVWp6QF4wXx3LzQzBr_EBDdH0CwhUmt_rhThj0rp_VVgQ3yr0TV7UAQGaZEO-kMJblMUjhxhK7ZMBlFXC2g7pt9zuvXhdX32QOJQ-xc20vA_RX-Kkp-Nkv3JmNCSsf-jxfwECOr_mKN7_uj7MiPK6_6zUQyrfOMKRHZcJoTlSqZylCaRn1CDxnN-1aTQ_RTplC_E27KD2TYykqc-aUGr0vkRnOGfezP3vFGNvofTfCnzZcgNJs_yTSCim1nkD5CKZEjyIhQ&publicKey=%7B%0A%20%20%20%20%20%20%22use%22%3A%20%22sig%22%2C%0A%20%20%20%20%20%20%22kty%22%3A%20%22RSA%22%2C%0A%20%20%20%20%20%20%22kid%22%3A%20%22b6d33bf1-59d5-4abf-9646-d1f321675f2b%22%2C%0A%20%20%20%20%20%20%22alg%22%3A%20%22RS256%22%2C%0A%20%20%20%20%20%20%22n%22%3A%20%22yFM_NznB3GdBMNJI9YGBmzGRBx3qkTzBfReOOq2DXRBNCkoZZOMSlfv-qqruo-pfbaLwPoz2pww81h9R2hcpoZUbaLb5R3rHOmIYftjiorjzjiLnFlndY5rq3foLxZxcZ6dYBDyS3qzZgf8hUs3CH__kG4MNAAD1Hoj8pER-_jFsAyVLBXpNrIy2aiuUscnFuOtK06LbfX0OjasKSKnx_IGXMje_uA2xziA5AUy5sm4wHWhcFWNCHNH5IgP-AHmg19lm7Swd_OlFfhxg43A7AfypV4-OdBb4qhReEgNr6Fnl761gELYfgDxGZXh6o3vs-V6s4g3fMGNGk4JYHFkkCvlclAg9XaprWKFbnhA8-elqqKWNIShf32uTcajf9rMWh_4M-mLiOPDTaJsZg0_f1z-MyE2_MDe-aRURYVY7tlQYF5MnY-Hg5uxn1QEghezclPe5YWUOtI39u__DgA6X1bgQlE5n4SmvyBTuFScxTEqsidwSiIY5hJh0ek60ds16V3I-XafQ9I6JmQ1TRHxdJfpAxYH5CbroSn7lRA-Tlqd-iaJ3ZHTIBuPUa2kZGwSE8zkFPyd803FhqM-cJTtoDIS0piNd8tn6d_-KJ62jWxJeyCWiOqMvTLHdobz0p9u181l2DU7qR07J1g4qtG0pPQfs6931hLZrG6gXsd2CYfs%22%2C%0A%20%20%20%20%20%20%22e%22%3A%20%22AQAB%22%0A%20%20%20%20%7D). - -## See it work - -A walk through the authorization code flow with OpenID Connect, from login to real -tokens. Run `./gradlew bootTestRun` and open http://localhost:8080 to click through it -yourself. - -Log in (the demo credentials are pre-filled): - -![login screen](docs/images/authorization-code-flow/1-login.png) - -Authorize the scopes the application is requesting: - -![consent screen](docs/images/authorization-code-flow/2-consent.png) - -Hydra redirects back with an authorization code, which the app exchanges for tokens: +
-![callback screen](docs/images/authorization-code-flow/3-callback-page.png) +## Reference -The result is a real access token, ID token, and refresh token from Hydra: +### Technologies used -![token response](docs/images/authorization-code-flow/4-tokens.png) +- Java 21 +- Spring Boot +- Gradle +- Testcontainers +- Ory Hydra +- Docker +- Freemarker +- Lombok +- GitHub Actions -## Building against a different testcontainers-ory-hydra version +### Building against a different testcontainers-ory-hydra version The `testcontainersOryHydraVersion` Gradle property overrides the version pinned in `gradle/libs.versions.toml`: @@ -282,17 +221,3 @@ instead, use a Gradle composite build: ``` ./gradlew build --include-build ../testcontainers-ory-hydra ``` - -## Task List - -- [ ] Add a fancier UI -- [ ] Refactor controller/service logic to be more consistent -- [ ] Tests to mock Ory Hydra responses -- [x] Allow rejecting on consent screen -- [ ] Add more unit tests -- [ ] Document playwright usage -- [ ] Show login errors on login screen -- [ ] Add playwright traces https://playwright.dev/java/docs/trace-viewer-intro -- [x] Log out -- [ ] Add example with Ory Cloud -