Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Static redirect URIs required by OIDC specification #140

Open
3 tasks
almereyda opened this issue Apr 14, 2024 · 13 comments
Open
3 tasks

Static redirect URIs required by OIDC specification #140

almereyda opened this issue Apr 14, 2024 · 13 comments

Comments

@almereyda
Copy link

almereyda commented Apr 14, 2024

The current implementation of the OIDC client in the ssh3 client binary spawns a local HTTP webserver at a random port with a random URL path fragment. This contradicts the OIDC specification, which expects a persistent and static (list of) redirect URI(s), even when the application is set to confidential.

randomSecretUrl := hex.EncodeToString(randomSecretUrlBytes[:])
listener, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
path := fmt.Sprintf("/ssh/%s", randomSecretUrl)
listeningPort := listener.Addr().(*net.TCPAddr).Port
secretUrl := fmt.Sprintf("http://localhost:%d%s", listeningPort, path)

3.1.2.1. Authentication Request
redirect_uri
REQUIRED. Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison). When using this flow, the Redirection URI SHOULD use the https scheme; however, it MAY use the http scheme, provided that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0, and provided the OP allows the use of http Redirection URIs in this case. Also, if the Client is a native application, it MAY use the http scheme with localhost or the IP loopback literals 127.0.0.1 or [::1] as the hostname. The Redirection URI MAY use an alternate scheme, such as one that is intended to identify a callback into a native application.

This way it is not possible to set up an OIDC client application with an ID and secret pair that works, e.g. with GitLab.

OIDC Provider:

  • Application ID: $(openssl rand -hex 32), is generated by the identity provider, e.g. 6f1379417c8a5cae738bc030e5ddc59ec352af02704351e182727fb0136fe1fe
  • Secret: gloas-$(openssl rand -hex 32), is generated in the identity provider, here GitLab, e.g. gloas-48ac71b03fcf805a127ba614c88646956751e154c76b77991f909a918c21b86e
  • Callback URL: http://localhost/
  • Trusted: No
  • Confidential: Yes
  • Scopes: openid, email

Notes:

  • Confidential can also be set to No, since we are serving the redirect URI from localhost. Then PKCE authentication is required.
  • The scopes must be matching with what is requested by the client:
    Scopes: []string{"openid email"},

Server configuration:

~/.ssh3/authorized_identities
oidc 6f1379417c8a5cae738bc030e5ddc59ec352af02704351e182727fb0136fe1fe https://gitlab.example.com [email protected]

Client configuration:

~/.ssh3/oidc_config.json
[
    {
        "issuer_url": "https://gitlab.example.com",
        "client_id": "<Application ID from above>", // e.g. 6f1379417c8a5cae738bc030e5ddc59ec352af02704351e182727fb0136fe1fe
        "client_secret": "<Secret from above>" // e.g. gloas-48ac71b03fcf805a127ba614c88646956751e154c76b77991f909a918c21b86e
    }
]

Note: The Readme speaks of config.json and also oidc_config.json, while the latter is the default when not using the -oidc-config flag.

ssh3/README.md

Line 262 in 20f2894

Below is an example `config.json` file for use with a Google account. This configuration file is an array

defaultFileName := path.Join(ssh3Dir, "oidc_config.json")

The SSH3 server has been started first with

ssh3-server --bind '[::]:8443' -cert ~/ssh3-cert.pem -key ~/ssh3-priv.key -generate-selfsigned-cert -v

to generate the key pair.

Subsequent invocations fail, when the -generate-selfsigned-cert flag is passed, why the final command line reads:

ssh3-server --bind '[::]:8443' -cert ~/ssh3-cert.pem -key ~/ssh3-priv.key -v

When spawning an associated SSH3 client with

ssh3 -use-oidc https://gitlab.example.com -v server:8443/ssh3-term

the OIDC provider, here GitLab, responds with:

An error has occurred

The redirect URI included is not valid.

This also happens when using the PKCE authentication scheme:

ssh3 -use-oidc https://gitlab.example.com -do-pkce -v server:8443/ssh3-term

Primary ways of mitigating this could be:

  • Supporting the dynamic client registration protocol: Final: OpenID Connect Dynamic Client Registration 1.0 incorporating errata set 2
  • Using a static redirect URI on the client which can be preconfigured in the OIDC provider, e.g. by a convention for port and secret, or by allowing to configure them directly.
  • Providing a complete example in the documentation or in the Readme that explains the whole life cycle of setting up and configuring OIDC-based SSH3 logins at the Identity Provider (IdP; OIDC provider), in the SSH3 server and in the SSH3 client.

Additionally there may be a way to get rid of the client_secret on the client in case of only supporting PKCE authentication for non-confidential OIDC clients, e.g. by introducing a flat -require-pkce on the server.

References:

@EthanHeilman
Copy link
Contributor

EthanHeilman commented May 16, 2024

I was going to open a ticket but I see there is already an existing ticket.

The Google OP requires a static redirectURI and does not allow wildcards. Google does let you wildcard localhost ports. The redirect URI http://localhost/ will work for all ports.

Google OP configuration

This is the error I get when I use SSH3 oidc with google:
oidc google

What is the security goal?

Is the security goal of adding a random fragment to securely link the request to the OP with the response form the OP? If so OIDC already provides a more secure way of doing this using PKCE (RFC 7636: Proof Key for Code Exchange). The way it works is client creates:

$$CV \gets \{0,1\}^{256}$$

$$VC \gets \mbox{SHA256(CV)}$$

The client then adds VC to the auth request URI. Then to claim the ID Token associated with this request, the client must reveal VC to server. This means that even if an attacker intercepts the original request, the attacker only learns VC not CV. Additionally if the attacker triggers a redirectURI response which is not intended response from the client, when the client-supplies VC, it will break.

It looks like SSH3 supports PKCE, but the default in the SSH3 config is to for PKCE to be false.

doPKCE := flag.Bool("do-pkce", false, "if set perform PKCE challenge-response with oidc")

I don't see a reason why you would ever want PKCE to be false. Is there an OP that doesn't support PKCE?

Port diversity with known ports

In OpenPubkey we support choosing a port from a list of ports. This allows parties to add that list of ports to their redirectURI list if needed and also allows us to fail over if another application is already bound to that port. It might be worth copying this pattern to support OPs that don't wildcard localhost ports.

https://github.com/openpubkey/openpubkey/blob/024d958d6c26e0883c9a9e15cd37a123ce16fd7e/providers/google.go#L406-L414

@almereyda almereyda changed the title Static redirect URIs required by OIDC specification (e.g. for compatibility with GitLab) Static redirect URIs required by OIDC specification May 16, 2024
@septatrix
Copy link

The random port is also problematic for firewalls

@EthanHeilman
Copy link
Contributor

@almereyda If you want to play around SSH OIDC with a fix redirectURI, I created a PR #144 You can just pull from my branch and use it.

@francoismichel
Copy link
Owner

One reason for using that randomly-generated path was to avoid other programs to hijack the token by sending a fake request to the ssh3 waiting on localhost, causing it to close the socket, and then the attacker could listen on the same port and retrieve the token like that. It seems doable on pretty much any OS.
But maybe am I missing something making such an attack undoable ?

@EthanHeilman
Copy link
Contributor

EthanHeilman commented May 31, 2024

@francoismichel If I understand your attack correctly, this was the attack that PKCE was introduced to defeat:

  1. The clients chooses initiating the auth request chooses a value CV and VC=Hash(CV) and initiates the auth request by sending VC to the OpenID Provider (OP),
  2. The user authenticates
  3. The OpenID Provider does the redirect to the redirect URI sending the auth code to the client via the localhost socket,
  4. The client sends the auth code and the CV value to the OpenID Provider which returns the ID Token.

An attacker who intercepts the auth code by listening on the localhost socket can not redeem that auth code for an ID Token because the attacker does not know a value CV such that VC=Hash(CV).

This is the value of enabling PKCE.


There is another known attack here which neither PKCE nor a randomized redirect URI does not fix.

  1. Attacker binds to a localhost port,
  2. Attacker opens browser to initiate auth request,
  3. User clicks I consent (in some IDPs that don't even need to consent if they have already consented for that client-id),
  4. Attacker gets auth code and redeems it for the ID Token

The user looking at a browser window that popped up has no easy way to determine that this was opened by the legitimate service running on localhost. There two mitigations:

  • Use a port numbers lower than 1024 in your redirect URI ACL at the OpenID Provider. This means that an attacker requires root access to bind to a port that will accept by the redirect URI ACL. This also means that SSH3 client needs to setUID root functionality.
  • ACL say three ports in your redirect URIs, then use a daemon to bind to all of these ports and ensure no one can port to them. This approach rests on the ability of the deamon to always be listening on those ports. Not a great solution.
  • Use a cosigner like we do in OpenPubkey

Mobile device flows don't have this problem because app identity is known to the OS and so the iOS can enforce a map of redirect URIs to specific cryptographic identities associated with the candy crush app. There are some other solutions in this space, but using I favor the OpenPubkey cosigner approach.

In general it is very hard to protect secrets like this if an attacker has a programming process on your endpoint that can open ports. That can do so much bad stuff, that provided security against such a threat is diminishing returns.

@septatrix
Copy link

Why release the port at all? To be the same redirect URL and work best with reverse proxies and firewalls the port should be static anyhow. So I see no reason why the port should be release after using it?

@EthanHeilman
Copy link
Contributor

@septatrix Releasing the port makes sense if you are using this as a cli where the cli isn't always running. The alternative is a daemon which starts at OS bootup and always holds that port or a set of ports, but that is a more complex client to build.

@septatrix
Copy link

@septatrix Releasing the port makes sense if you are using this as a cli where the cli isn't always running. The alternative is a daemon which starts at OS bootup and always holds that port or a set of ports, but that is a more complex client to build.

Ah I was under the assumption that the SSH3 server would be the one receiving the token. That seems like the intuitive solution

@francoismichel
Copy link
Owner

Thanks for the discussion.
The attack with an attacker opening a browser window concurrently to SSH3 may not be easy to perform but it makes sense. I wish there was a generic way in OIDC to display some text to the user in the browser window so that it can check the browser window is indeed the right one.
The difference in this scenario however, is that the attacker must either be root or be running with the same rights as the current user in order to be able to show a browser window in its desktop session, while the previous scenario (the one avoidable with PKCE), the attacker just needs to be able to listen on a port.

The reason why PKCE is disabled by default is simply because I couldn't make it to work by the time with that specific version of the oidc module and the google OP, but I may give it another try now. We should probably list the common OPs that support PKCE well and enable it by default.

@francoismichel
Copy link
Owner

Concerning the random URL, I am not strongly against removing it, but we should probably ensure that PKCE is enabled then, and use the random URL if not.

@septatrix
Copy link

I would also like to note that the RFC says to construct these loopback URLs using the numeric literals (127.0.0.1 and [::1] respectively) instead of "localhost".

Another option (which would also be necessary when one wants to implement a mobile client for SSH3) is to use private-use URI schemes like be.francoismichel.ssh3:/oauth2redirect/. This is also possible on desktop OS though it requires a more traditional installation. E.g. on linux a ssh3.desktop file would be required

@EthanHeilman
Copy link
Contributor

@septatrix My two cents:

  1. Using numeric loopbacks seems reasonable. The only reason I like localhost is that is it generic to ipv4 and ipv6. It seems like it wouldn't be hard to just specify two redirect URIs one being the loopback for IPv4 and one being the loopback for IPv6.
  2. How well supported is native-app OAuth in OIDC on the desktop? I haven't build any systems using it and I don't know if there are any gotchas. I agree on mobile devices private-use schemes is both well supported and a security requirement. Are protocol handlers still used for this?

@septatrix
Copy link

@septatrix My two cents:

  1. Using numeric loopbacks seems reasonable. The only reason I like localhost is that is it generic to ipv4 and ipv6. It seems like it wouldn't be hard to just specify two redirect URIs one being the loopback for IPv4 and one being the loopback for IPv6.

In Linux binding to ::1 defaults to dual stack mode, i.e. it also received IPv4 packets

  1. How well supported is native-app OAuth in OIDC on the desktop? I haven't build any systems using it and I don't know if there are any gotchas. I agree on mobile devices private-use schemes is both well supported and a security requirement. Are protocol handlers still used for this?

Not really sure about this TBH

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

No branches or pull requests

4 participants