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

Support form_post mode #98

Merged
merged 4 commits into from
Feb 15, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
Copy link
Contributor

@tyrannosaurus-becks tyrannosaurus-becks Feb 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing closing " at end.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is addressed in #99 if we merge it.

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