Skip to content

Conversation

@snazy
Copy link
Member

@snazy snazy commented May 14, 2025

Auth-code-flow test recently started to fail in CI for all PRs. Difficult to tell which change really causes the issues here.

Background: org.projectnessie.client.auth.oauth2.AuthorizationCodeFlow binds to the inet-address returned by InetAddress.getLoopbackAddress() and the redirect-URL uses localhost.

"As usual", the affected test ITOAuth2ClientAuthelia works fine locally. However, there's a little difference in the environment. In GH workflows, localhost resolves to both ::1 and 127.0.0.1, whereas locally localhost only resolves to 127.0.0.1. So it feels related to IPv4 vs IPv6.

Some experiments with different contents in /etc/hosts:

::1 localhost
127.0.0.1 localhost

--> test passes

127.0.0.1 localhost
::1 localhost

--> test passes

127.0.0.1 localhost

--> test passes

::1 localhost

--> test fails

This means that the issue is defintely related to which IP address (127.0.0.1 or ::1) localhost resolves to.

The change in this PR is to start one HTTP server for all loopback IP addresses.

Alternatives were:

  • Bind to "all addresses": this feels a bit insecure, because it would open the http server to remote requests
  • Inspect which interfaces have localhost in InetAddress.getHostName(). This performs a roundtrip to at least the local resolver, but more importantly it can still yield multiple addresses.
  • Resolve localhost via InetAddress.get[All]ByName(). This performs a name service roundtrip (usually only the local resolver, but still) - but it can also yield multiple addresses.
  • Use the IP address (127.0.0.1 or ::1 or whatever host address InetAddress.getLoopbackAddress().getHostAddress() returns) in the redirect URL. This would require changes to the allowed redirect-URLs in Authelia/Keycloak configurations. On top, some implementations have issues parsing IPv6 addresses in URLs (e.g. http://[::1]:54321/foo/bar) due to the repeated :.
  • Inspect /etc/hosts - it's another can of worms especially on all the different operating system. So getting into that area for this use case feels wrong.

Side note: the order in which resolvers return an IP address is up to the resolver. There are no guarantees from a client's side of view (beside that some IP address, if resolvable, is returned).

Overall the approach to listen to all loopback addresses seems to be the best approach.

Additional debug logging has been added.

…ost`

Auth-code-flow test recently started to fail in CI for all PRs. Difficult to tell which change really causes the issues here.

Background: `org.projectnessie.client.auth.oauth2.AuthorizationCodeFlow` binds to the inet-address returned by `InetAddress.getLoopbackAddress()` and the redirect-URL uses `localhost`.

"As usual", the affected test `ITOAuth2ClientAuthelia` works fine locally. However, there's a little difference in the environment. In GH workflows, `localhost` resolves to both ::1 and 127.0.0.1, whereas locally `localhost` only resolves to 127.0.0.1. So it feels related to IPv4 vs IPv6.

Some experiments with different contents in `/etc/hosts`:
```
::1 localhost
127.0.0.1 localhost
```
--> test passes
```
127.0.0.1 localhost
::1 localhost
```
--> test passes
```
127.0.0.1 localhost
```
--> test passes
```
::1 localhost
```
--> test fails

This means that the issue is defintely related to which IP address (`127.0.0.1` or `::1`) `localhost` resolves to.

The change in this PR is to start one HTTP server for all loopback IP addresses.

Alternatives were:
* Bind to "all addresses": this feels a bit insecure, because it would open the http server to remote requests
* Inspect which interfaces have `localhost` in `InetAddress.getHostName()`. This performs a roundtrip to at least the local resolver, but more importantly it can still yield multiple addresses.
* Resolve `localhost` via `InetAddress.get[All]ByName()`. This performs a name service roundtrip (_usually_ only the local resolver, but still) - but it can also yield multiple addresses.
* Use the IP address (`127.0.0.1` or `::1` or whatever host address `InetAddress.getLoopbackAddress().getHostAddress()` returns) in the redirect URL. This would require changes to the allowed redirect-URLs in Authelia/Keycloak configurations. On top, some implementations have issues parsing IPv6 addresses in URLs (e.g. `http://[::1]:54321/foo/bar`) due to the repeated `:`.
* Inspect `/etc/hosts` - it's another can of worms especially on all the different operating system. So getting into that area for this use case feels wrong.

Side note: the order in which resolvers return an IP address is up to the resolver. There are no guarantees from a client's side of view (beside that some IP address, if resolvable, is returned).

Overall the approach to listen to all loopback addresses seems to be the best approach.

Additional debug logging has been added.
@snazy snazy marked this pull request as draft May 14, 2025 11:39
@snazy
Copy link
Member Author

snazy commented May 14, 2025

Meh - even this approach didn't help :(

@snazy snazy force-pushed the auth-code-multiple-localhost branch from f210064 to 32e5139 Compare May 14, 2025 12:02
@snazy snazy force-pushed the auth-code-multiple-localhost branch from 32e5139 to 067e59e Compare May 14, 2025 12:10
@snazy
Copy link
Member Author

snazy commented May 14, 2025

Aha!

java.lang.AssertionError: 	
Expecting path of	
  <https://127.0.0.1:32783/consent/openid/decision?flow=openid_connect&flow_id=4bcebab8-c505-4954-b639-957bf9cdf9bb>	
to be:	
  <"/nessie-client/auth">	
but was:	
  <"/consent/openid/decision">	
	at org.projectnessie.client.auth.oauth2.AutheliaAuthorizationCodeResourceOwnerEmulator.invokeCallbackUrl(AutheliaAuthorizationCodeResourceOwnerEmulator.java:119)	
	at org.projectnessie.client.auth.oauth2.AutheliaAuthorizationCodeResourceOwnerEmulator.triggerAuthorizationCodeFlow(AutheliaAuthorizationCodeResourceOwnerEmulator.java:68)	
	at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1804)	
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)	
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)	
	at java.base/java.lang.Thread.run(Thread.java:1583)	

The assertion-error was never exposed.

@snazy
Copy link
Member Author

snazy commented May 14, 2025

Looks like it's an issue w/ Authelia and the requested offline_access scope, because of If the client requests the offline_access or offline scope the mode will automatically be explicit regardless of client configuration..

But this doesn't really explain why the test passes locally but not in GHA.

@snazy snazy force-pushed the auth-code-multiple-localhost branch from 75be204 to 52d3343 Compare May 14, 2025 12:59
@snazy
Copy link
Member Author

snazy commented May 14, 2025

Maybe @adutra has an idea here. I'm a bit lost.

snazy added a commit to snazy/nessie that referenced this pull request May 16, 2025
See also projectnessie#10818, especially this comment:
Looks like it's an issue w/ Authelia and the requested `offline_access` scope, because of [`If the client requests the offline_access or offline scope the mode will automatically be explicit regardless of client configuration.`](https://www.authelia.com/configuration/identity-providers/openid-connect/clients/#implicit).
But this doesn't really explain why the test passes locally but not in GHA.
snazy added a commit that referenced this pull request May 16, 2025
See also #10818, especially this comment:
Looks like it's an issue w/ Authelia and the requested `offline_access` scope, because of [`If the client requests the offline_access or offline scope the mode will automatically be explicit regardless of client configuration.`](https://www.authelia.com/configuration/identity-providers/openid-connect/clients/#implicit).
But this doesn't really explain why the test passes locally but not in GHA.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant