Skip to content

Commit

Permalink
Support form_post mode
Browse files Browse the repository at this point in the history
form_post is not a commonly used mode but is needed for certain
applications. Adding support for it impacted both the CLI and UI handler,
as well as Vault (hashicorp/vault#8325).

In addition to allowing configuration of `response_mode=form_post`, the
operator can now also choose to configure `response_types`, which today
is always `code`. This PR allows adding the `id_token` type as well. This
is only supported if form_post is enabled.

A curious aspect of this PR is to respond with some Javascript that
writes to localStorage. This is a effectively doing what the UI does
after a redirect, and allows the form_post support without modifying the
UI code at all. There are cleaner options, but they would require a
significant update to the UI.
  • Loading branch information
Jim Kalafut committed Feb 13, 2020
1 parent 026b6c9 commit 6a28000
Show file tree
Hide file tree
Showing 9 changed files with 489 additions and 74 deletions.
72 changes: 49 additions & 23 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"os/signal"
"path"
"regexp"
"runtime"
"strings"
Expand Down Expand Up @@ -78,28 +79,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
}

// Set up callback handler
http.HandleFunc("/oidc/callback", func(w http.ResponseWriter, req *http.Request) {
var response string

query := req.URL.Query()
code := query.Get("code")
state := query.Get("state")
data := map[string][]string{
"code": {code},
"state": {state},
}

secret, err := c.Logical().ReadWithData(fmt.Sprintf("auth/%s/oidc/callback", mount), data)
if err != nil {
summary, detail := parseError(err)
response = errorHTML(summary, detail)
} else {
response = successHTML
}

w.Write([]byte(response))
doneCh <- loginResp{secret, err}
})
http.HandleFunc("/oidc/callback", callbackHandler(c, mount, doneCh))

listener, err := net.Listen("tcp", listenAddress+":"+port)
if err != nil {
Expand Down Expand Up @@ -132,6 +112,52 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
}
}

func callbackHandler(c *api.Client, mount string, doneCh chan<- loginResp) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
var response string
var secret *api.Secret
var err error

defer func() {
w.Write([]byte(response))
doneCh <- loginResp{secret, err}
}()

// Pull any parameters from either the body or query parameters.
// FormValue prioritizes body values, if found.
data := map[string][]string{
"state": {req.FormValue("state")},
"code": {req.FormValue("code")},
"id_token": {req.FormValue("id_token")},
}

// If this is a POST, then the form_post response_mode is being used and the flow
// involves an extra step. First GET the data to Vault, and then issue a GET with
// the same state/code to complete the auth as normal.
if req.Method == http.MethodPost {
url := c.Address() + path.Join("/v1/auth", mount, "oidc/callback")
resp, err := http.PostForm(url, data)
if err != nil {
summary, detail := parseError(err)
response = errorHTML(summary, detail)
return
}
defer resp.Body.Close()

// An id_token will never be part of a redirect GET, so remove it here too.
delete(data, "id_token")
}

secret, err = c.Logical().ReadWithData(fmt.Sprintf("auth/%s/oidc/callback", mount), data)
if err != nil {
summary, detail := parseError(err)
response = errorHTML(summary, detail)
} else {
response = successHTML
}
}
}

func fetchAuthURL(c *api.Client, role, mount, callbackport string, callbackMethod string, callbackHost string) (string, error) {
var authURL string

Expand All @@ -150,7 +176,7 @@ func fetchAuthURL(c *api.Client, role, mount, callbackport string, callbackMetho
}

if authURL == "" {
return "", errors.New(fmt.Sprintf("Unable to authorize role %q. Check Vault logs for more information.", role))
return "", fmt.Errorf("Unable to authorize role %q. Check Vault logs for more information.", role)
}

return authURL, nil
Expand Down
145 changes: 145 additions & 0 deletions cli_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,148 @@ h1 + p {
`
return fmt.Sprintf(html, summary, detail)
}

func formpostHTML(path, code, state string) string {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Complete sign-in process</title>
<style>
body {
font-size: 14px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
}
hr {
border-color: #fdfdfe;
margin: 24px 0;
}
.container {
display: flex;
justify-content: center;
align-items: center;
height: 70vh;
}
#logo {
display: block;
fill: #6f7682;
margin-bottom: 16px;
}
.message {
display: flex;
min-width: 40vw;
background: #fafdfa;
border: 1px solid #c6e9c9;
margin-bottom: 12px;
padding: 12px 16px 16px 12px;
position: relative;
border-radius: 2px;
font-size: 14px;
}
.message-content {
margin-left: 4px;
}
.message #checkbox {
fill: #2eb039;
}
.message .message-title {
color: #1e7125;
font-size: 16px;
font-weight: 700;
line-height: 1.25;
}
.message .message-body {
border: 0;
margin-top: 4px;
}
.message p {
font-size: 12px;
margin: 0;
padding: 0;
color: #17421b;
}
a {
display: block;
margin: 8px 0;
color: #1563ff;
text-decoration: none;
font-weight: 600;
}
a:hover {
color: black;
}
a svg {
fill: currentcolor;
}
.icon {
align-items: center;
display: inline-flex;
justify-content: center;
height: 21px;
width: 21px;
vertical-align: middle;
}
h1 {
font-size: 17.5px;
font-weight: 700;
margin-bottom: 0;
}
h1 + p {
margin: 8px 0 16px 0;
}
</style>
</head>
<body translate="no" >
<div class="container">
<div>
<svg id="logo" width="146" height="51" viewBox="0 0 146 51" xmlns="http://www.w3.org/2000/svg">
<g id="vault-logo-v" fill-rule="nonzero">
<path d="M0,0 L25.4070312,51 L51,0 L0,0 Z M28.5,10.5 L31.5,10.5 L31.5,13.5 L28.5,13.5 L28.5,10.5 Z M22.5,22.5 L19.5,22.5 L19.5,19.5 L22.5,19.5 L22.5,22.5 Z M22.5,18 L19.5,18 L19.5,15 L22.5,15 L22.5,18 Z M22.5,13.5 L19.5,13.5 L19.5,10.5 L22.5,10.5 L22.5,13.5 Z M26.991018,27 L24,27 L24,24 L27,24 L26.991018,27 Z M26.991018,22.5 L24,22.5 L24,19.5 L27,19.5 L26.991018,22.5 Z M26.991018,18 L24,18 L24,15 L27,15 L26.991018,18 Z M26.991018,13.5 L24,13.5 L24,10.5 L27,10.5 L26.991018,13.5 Z M28.5,15 L31.5,15 L31.5,18 L28.5089552,18 L28.5,15 Z M28.5,22.5 L28.5,19.5 L31.5,19.5 L31.5,22.4601182 L28.5,22.5 Z"></path>
</g>
<path id="vault-logo-name" d="M69.7218638,30.2482468 L63.2587814,8.45301543 L58,8.45301543 L65.9885305,34.6072931 L73.4551971,34.6072931 L81.4437276,8.45301543 L76.1849462,8.45301543 L69.7218638,30.2482468 Z M97.6329749,22.0014025 C97.6329749,17.2103787 95.8265233,15.0897616 89.6845878,15.0897616 C87.5168459,15.0897616 84.8272401,15.4431978 82.9806452,15.9929874 L83.5827957,19.6451613 C85.3089606,19.2917251 87.2358423,19.056101 89.0021505,19.056101 C92.1333333,19.056101 92.7354839,19.802244 92.7354839,21.9228612 L92.7354839,23.9256662 L88.0387097,23.9256662 C84.0645161,23.9256662 82.3383513,25.4179523 82.3383513,29.3057504 C82.3383513,32.6044881 83.8637993,35 87.4365591,35 C89.4035842,35 91.4910394,34.4502104 93.2573477,33.3113604 L93.618638,34.6072931 L97.6329749,34.6072931 L97.6329749,22.0014025 Z M92.7354839,30.2089762 C91.8121864,30.7194951 90.4874552,31.1907433 89.0422939,31.1907433 C87.5168459,31.1907433 87.0752688,30.601683 87.0752688,29.2664797 C87.0752688,27.8134642 87.5168459,27.3814867 89.1225806,27.3814867 L92.7354839,27.3814867 L92.7354839,30.2089762 Z M102.421505,15.4824684 L102.421505,29.345021 C102.421505,32.7615708 103.585663,35 106.837276,35 C109.125448,35 112.216487,34.1753156 114.665233,32.997195 L115.146953,34.6072931 L118.880287,34.6072931 L118.880287,15.4824684 L113.982796,15.4824684 L113.982796,28.7559607 C112.216487,29.6591865 110.088889,30.3660589 108.884588,30.3660589 C107.760573,30.3660589 107.318996,29.85554 107.318996,28.8345021 L107.318996,15.4824684 L102.421505,15.4824684 Z M129.168459,34.6072931 L129.168459,7 L124.270968,7.66760168 L124.270968,34.6072931 L129.168459,34.6072931 Z M144.394265,30.601683 C143.551254,30.8373072 142.6681,30.9943899 141.94552,30.9943899 C140.660932,30.9943899 140.179211,30.3267882 140.179211,29.3057504 L140.179211,19.2917251 L144.875986,19.2917251 L145.197133,15.4824684 L140.179211,15.4824684 L140.179211,10.0631136 L135.28172,10.7307153 L135.28172,15.4824684 L132.351254,15.4824684 L132.351254,19.2917251 L135.28172,19.2917251 L135.28172,29.9340813 C135.28172,33.3506311 137.088172,35 140.660932,35 C141.905376,35 143.912545,34.6858345 144.956272,34.2538569 L144.394265,30.601683 Z"></path>
</svg>
<div class="message is-success">
<svg id="checkbox" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512">
<path d="M256 32C132.3 32 32 132.3 32 256s100.3 224 224 224 224-100.3 224-224S379.7 32 256 32zm114.9 149.1L231.8 359.6c-1.1 1.1-2.9 3.5-5.1 3.5-2.3 0-3.8-1.6-5.1-2.9-1.3-1.3-78.9-75.9-78.9-75.9l-1.5-1.5c-.6-.9-1.1-2-1.1-3.2 0-1.2.5-2.3 1.1-3.2.4-.4.7-.7 1.1-1.2 7.7-8.1 23.3-24.5 24.3-25.5 1.3-1.3 2.4-3 4.8-3 2.5 0 4.1 2.1 5.3 3.3 1.2 1.2 45 43.3 45 43.3l111.3-143c1-.8 2.2-1.4 3.5-1.4 1.3 0 2.5.5 3.5 1.3l30.6 24.1c.8 1 1.3 2.2 1.3 3.5.1 1.3-.4 2.4-1 3.3z"></path>
</svg>
<div class="message-content">
<div class="message-title">
Completing the sign-in process...
</div>
</div>
</div>
<hr />
<h1>Not sure how to get started?</h1>
<p class="learn">
Check out beginner and advanced guides on HashiCorp Vault at the HashiCorp Learn site or read more in the official documentation.
</p>
<a href="https://learn.hashicorp.com/vault" rel="noreferrer noopener">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.338 2.255a.79.79 0 0 0-.645 0L.657 5.378c-.363.162-.534.538-.534.875 0 .337.171.713.534.875l1.436.637c-.332.495-.638 1.18-.744 2.106a.887.887 0 0 0-.26 1.559c.02.081.03.215.013.392-.02.205-.074.43-.162.636-.186.431-.45.64-.741.64v.98c.651 0 1.108-.365 1.403-.797l.06.073c.32.372.826.763 1.455.763v-.98c-.215 0-.474-.145-.71-.42-.111-.13-.2-.27-.259-.393a1.014 1.014 0 0 1-.06-.155c-.01-.036-.013-.055-.013-.058h-.022a2.544 2.544 0 0 0 .031-.641.886.886 0 0 0-.006-1.51c.1-.868.398-1.477.699-1.891l.332.147-.023.746v2.228c0 .115.04.22.105.304.124.276.343.5.587.677.297.217.675.396 1.097.54.846.288 1.943.456 3.127.456 1.185 0 2.281-.168 3.128-.456.422-.144.8-.323 1.097-.54.244-.177.462-.401.586-.677a.488.488 0 0 0 .106-.304V8.218l2.455-1.09c.363-.162.534-.538.534-.875 0-.337-.17-.713-.534-.875L8.338 2.255zm-.34 2.955L3.64 7.38l4.375 1.942 6.912-3.069-6.912-3.07-6.912 3.07 1.665.74 4.901-2.44.328.657zM14.307 1H12.5a.5.5 0 1 1 0-1h3a.499.499 0 0 1 .5.65V3.5a.5.5 0 1 1-1 0V1.72l-1.793 1.774a.5.5 0 0 1-.713-.701L14.307 1zm-2.368 7.653v2.383a.436.436 0 0 0-.007.021c-.017.063-.084.178-.282.322-.193.14-.473.28-.836.404-.724.247-1.71.404-2.812.404-1.1 0-2.087-.157-2.811-.404a3.188 3.188 0 0 1-.836-.404c-.198-.144-.265-.26-.282-.322a.437.437 0 0 0-.007-.02V8.983l.01-.338 3.617 1.605a.791.791 0 0 0 .645 0l3.6-1.598z" fill-rule="evenodd"></path>
</svg>
</span>
Get started with Vault
</a>
<a href="https://vaultproject.io/docs" rel="noreferrer noopener">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M13.307 1H11.5a.5.5 0 1 1 0-1h3a.499.499 0 0 1 .5.65V3.5a.5.5 0 1 1-1 0V1.72l-1.793 1.774a.5.5 0 0 1-.713-.701L13.307 1zM12 14V8a.5.5 0 1 1 1 0v6.5a.5.5 0 0 1-.5.5H.563a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 .5-.5H8a.5.5 0 0 1 0 1H1v12h11zM4 6a.5.5 0 0 1 0-1h3a.5.5 0 0 1 0 1H4zm0 2.5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1H4zM4 11a.5.5 0 1 1 0-1h5a.5.5 0 1 1 0 1H4z"/>
</svg>
</span>
View the official Vault documentation
</a>
</div>
</div>
<script>
window.localStorage.setItem("oidcState", JSON.stringify({"path":"%s", "code":"%s", "state":"%s"}));
</script>
</body>
</html>
`
return fmt.Sprintf(html, path, code, state)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ require (
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/ryanuber/go-glob v1.0.0
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
golang.org/x/sync v0.0.0-20190423024810-112230192c58
gopkg.in/square/go-jose.v2 v2.4.1
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
60 changes: 54 additions & 6 deletions path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@ import (
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
"golang.org/x/oauth2"
)

const (
responseTypeCode = "code" // Authorization code flow
responseTypeIDToken = "id_token" // ID Token for form post
responseModeQuery = "query" // Response as a redirect with query parameters
responseModeFormPost = "form_post" // Response as an HTML Form
)

func pathConfig(b *jwtAuthBackend) *framework.Path {
return &framework.Path{
Pattern: `config`,
Expand All @@ -41,6 +49,14 @@ func pathConfig(b *jwtAuthBackend) *framework.Path {
Sensitive: true,
},
},
"oidc_response_mode": {
Type: framework.TypeString,
Description: "The response mode to be used in the OAuth2 request. Allowed values are 'query' and 'form_post'.",
},
"oidc_response_types": {
Type: framework.TypeCommaStringSlice,
Description: "The response typess to request. Allowed values are 'code' and 'id_token'. Defaults to 'code'.",
},
"jwks_url": {
Type: framework.TypeString,
Description: `JWKS URL to use to authenticate signatures. Cannot be used with "oidc_discovery_url" or "jwt_validation_pubkeys".`,
Expand Down Expand Up @@ -101,22 +117,22 @@ func (b *jwtAuthBackend) config(ctx context.Context, s logical.Storage) (*jwtCon
return nil, nil
}

result := &jwtConfig{}
if err := entry.DecodeJSON(result); err != nil {
config := &jwtConfig{}
if err := entry.DecodeJSON(config); err != nil {
return nil, err
}

for _, v := range result.JWTValidationPubKeys {
for _, v := range config.JWTValidationPubKeys {
key, err := certutil.ParsePublicKeyPEM([]byte(v))
if err != nil {
return nil, errwrap.Wrapf("error parsing public key: {{err}}", err)
}
result.ParsedJWTPubKeys = append(result.ParsedJWTPubKeys, key)
config.ParsedJWTPubKeys = append(config.ParsedJWTPubKeys, key)
}

b.cachedConfig = result
b.cachedConfig = config

return result, nil
return config, nil
}

func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
Expand All @@ -133,6 +149,8 @@ func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques
"oidc_discovery_url": config.OIDCDiscoveryURL,
"oidc_discovery_ca_pem": config.OIDCDiscoveryCAPEM,
"oidc_client_id": config.OIDCClientID,
"oidc_response_mode": config.OIDCResponseMode,
"oidc_response_types": config.OIDCResponseTypes,
"default_role": config.DefaultRole,
"jwt_validation_pubkeys": config.JWTValidationPubKeys,
"jwt_supported_algs": config.JWTSupportedAlgs,
Expand All @@ -151,6 +169,8 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
OIDCDiscoveryCAPEM: d.Get("oidc_discovery_ca_pem").(string),
OIDCClientID: d.Get("oidc_client_id").(string),
OIDCClientSecret: d.Get("oidc_client_secret").(string),
OIDCResponseMode: d.Get("oidc_response_mode").(string),
OIDCResponseTypes: d.Get("oidc_response_types").([]string),
JWKSURL: d.Get("jwks_url").(string),
JWKSCAPEM: d.Get("jwks_ca_pem").(string),
DefaultRole: d.Get("default_role").(string),
Expand Down Expand Up @@ -227,6 +247,22 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
}
}

// Validate response_types
if !strutil.StrListSubset([]string{responseTypeCode, responseTypeIDToken}, config.OIDCResponseTypes) {
return logical.ErrorResponse("invalid response_types %v. 'code' and 'id_token' are allowed", config.OIDCResponseTypes), nil
}

// Validate response_mode
switch config.OIDCResponseMode {
case "", responseModeQuery:
if config.hasType(responseTypeIDToken) {
return logical.ErrorResponse("query response_mode may not be used with an id_token response_type"), nil
}
case responseModeFormPost:
default:
return logical.ErrorResponse("invalid response_mode: %q", config.OIDCResponseMode), nil
}

entry, err := logical.StorageEntryJSON(configPath, config)
if err != nil {
return nil, err
Expand Down Expand Up @@ -286,6 +322,8 @@ type jwtConfig struct {
OIDCDiscoveryCAPEM string `json:"oidc_discovery_ca_pem"`
OIDCClientID string `json:"oidc_client_id"`
OIDCClientSecret string `json:"oidc_client_secret"`
OIDCResponseMode string `json:"oidc_response_mode"`
OIDCResponseTypes []string `json:"oidc_response_types`
JWKSURL string `json:"jwks_url"`
JWKSCAPEM string `json:"jwks_ca_pem"`
JWTValidationPubKeys []string `json:"jwt_validation_pubkeys"`
Expand Down Expand Up @@ -321,6 +359,16 @@ func (c jwtConfig) authType() int {
return unconfigured
}

// hasType returns whether the list of response types includes the requested
// type. The default type is 'code' so that special case is handled as well.
func (c jwtConfig) hasType(t string) bool {
if len(c.OIDCResponseTypes) == 0 && t == responseTypeCode {
return true
}

return strutil.StrListContains(c.OIDCResponseTypes, t)
}

const (
confHelpSyn = `
Configures the JWT authentication backend.
Expand Down
Loading

0 comments on commit 6a28000

Please sign in to comment.