--- tags: api-design --- # Pinniped Supervisor `response_mode=form_post` Support ## Introduction This document describes a design to solve [pinniped/#668](https://github.com/vmware-tanzu/pinniped/issues/668) (_Support for OIDC logins on hosts without a local web browser ("jump host")_) by adding support for `response_mode=form_post` in the Pinniped Supervisor. ### What is `response_mode=form_post`? The `response_mode` parameter is defined in [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html). It is a counterpart to `response_type` which "informs the Authorization Server of the mechanism to be used for returning Authorization Response parameters from the Authorization Endpoint". The current behavior in Pinniped corresponds to `response_mode=query`, where the response is returned via an HTTP redirect with URL query parameters. The meaning of `response_mode=form_post` is defined by [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html). Instead of an HTTP redirect, the response parameters such as authorization code, state, and granted scopes are "encoded as HTML form values that are auto-submitted in the User Agent, and thus are transmitted via the HTTP POST method to the Client". There is also a well known field in the OIDC discovery document to advertise supported response modes (`response_modes_supported` defined in [OpenID Connect Discovery 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3)). ### How does this work in Fosite? Fosite supports this functionality since November 2020 ([fosite/#470](https://github.com/ory/fosite/issues/470)). The [default HTML template](https://github.com/ory/fosite/blob/0a48821b156f4a5dffa0f7149d30d5cf02636f37/authorize_helper.go#L37-L50) shows a minimal zero-interaction example of the response: ```go var FormPostDefaultTemplate = template.Must(template.New("form_post").Parse(`<html> <head> <title>Submit This Form</title> </head> <body onload="javascript:document.forms[0].submit()"> <form method="post" action="{{ .RedirURL }}"> {{ range $key,$value := .Parameters }} {{ range $parameter:= $value}} <input type="hidden" name="{{$key}}" value="{{$parameter}}"/> {{end}} {{ end }} </form> </body> </html>`)) ``` We can override this to provide our own HTML and JavaScript. We need to do a little bit of setup to tell Fosite that our `pinniped-cli` client is allowed to use this response type and that our authorization server is prepared to handle it. ## Tweaking `form_post` to support the "jump host" case The key observation of this design is that the HTML page in the `response_mode=form_post` response is also well-positioned to have extended behavior that allows for a choice of two login flows, selected dynamically by JavaScript: 1. *Successfully send* a POST request to the (localhost) callback endpoint then show a "success" message. 2. *Fail to send* a POST request to the (localhost) callback, then show a "please copy the auth code" message allowing the user to manually complete the login by copying the response parameters from the Supervisor-hosted page and pasting into the waiting CLI process. Note that this server behavior is still 100% compatible with standard OIDC clients in the first case. The second case extends beyond the spec but follows other known examples (e.g., the Concourse `fly` login flow). There are also corresponding changes to the `pinniped login oidc` command: - If the issuer advertises support for the `form_post` response mode (in OIDC discovery), then add `response_mode=form_post` as a extra parameter on the authorization request. - If `form_post` is selected: - Expect the callback to receive an HTTP POST instead of a GET, with parameters encoded in the request body rather than the URL query string. - If stdin is a TTY, print out a prompt to paste the authorization code. Spawn a goroutine that runs concurrently with the callback handler. This goroutine should accept a pasted authorization code and complete the login flow. ## Security The authorization code will now be copy-pasted by the user, which exposes it to being leaked by inadvertantly pasting it in the wrong window (e.g., Slack). However, this is well mitigated by several existing mechanisms: - Authorization codes expire quickly (10 minutes) and are single-use. - The authorization code is only valid with a corresponding PKCE verifier, which is generated and held in-memory by the client. These mechanisms also prevent a lot of possible phishing-style attacks. ## Questions - Is CORS sufficient for the cross-origin requests we're making? Any gotchas? - How much do we want to style this new page? - Should the page styling eventually be re-brandable (e.g., custom logo)?