owned this note changed 4 years ago
Published Linked with GitHub

Pinniped Supervisor response_mode=form_post Support

Introduction

This document describes a design to solve pinniped/#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. 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. 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).

How does this work in Fosite?

Fosite supports this functionality since November 2020 (fosite/#470). The default HTML template shows a minimal zero-interaction example of the response:

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)?
Select a repo