Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func backend() *jwtAuthBackend {
"login",
"oidc/auth_url",
"oidc/callback",
"oidc/manual",

// Uncomment to mount simple UI handler for local development
// "ui",
Expand Down
126 changes: 98 additions & 28 deletions cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jwtauth

import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
Expand All @@ -17,13 +18,17 @@ import (

"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/base62"
pwd "github.com/hashicorp/vault/sdk/helper/password"
)

const defaultMount = "oidc"
const defaultListenAddress = "localhost"
const defaultPort = "8250"
const defaultCallbackHost = "localhost"
const defaultCallbackMethod = "http"
const (
defaultMount = "oidc"
defaultListenAddress = "localhost"
defaultPort = "8250"
defaultCallbackHost = "localhost"
defaultCallbackMethod = "http"
defaultMode = "browser"
)

var errorRegex = regexp.MustCompile(`(?s)Errors:.*\* *(.*)`)

Expand Down Expand Up @@ -74,33 +79,84 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro

role := m["role"]

authURL, clientNonce, err := fetchAuthURL(c, role, mount, callbackPort, callbackMethod, callbackHost)
if err != nil {
return nil, err
mode, ok := m["mode"]
if !ok {
mode = defaultMode
} else {
switch mode {
case defaultMode, "manual":
// valid
default:
return nil, fmt.Errorf("%q is not a valid mode", mode)
}
}

// Set up callback handler
http.HandleFunc("/oidc/callback", callbackHandler(c, mount, clientNonce, doneCh))
if mode == "manual" {
// by default, the mount has a / suffix, which
// we remove to construct the appropriaate redirect_uri
if strings.HasSuffix(mount, "/") {
mount = mount[:len(mount)-1]
}
path := fmt.Sprintf("v1/auth/%s/oidc/manual", mount)
authURL, clientNonce, err := fetchAuthURL(c, role, mount, callbackPort, callbackMethod, callbackHost, path)
if err != nil {
return nil, err
}

listener, err := net.Listen("tcp", listenAddress+":"+port)
if err != nil {
return nil, err
}
defer listener.Close()
fmt.Fprintf(os.Stderr, "Complete the login via your OIDC provider by following this link in your browser: \n\n %s\n\n\n", authURL)
fmt.Fprintf(os.Stderr, "Enter verification code: ")
input, err := pwd.Read(os.Stdin)
fmt.Fprintf(os.Stderr, "\n")
if err != nil {
return nil, err
}

// Open the default browser to the callback URL.
fmt.Fprintf(os.Stderr, "Complete the login via your OIDC provider. Launching browser to:\n\n %s\n\n\n", authURL)
if err := openURL(authURL); err != nil {
fmt.Fprintf(os.Stderr, "Error attempting to automatically open browser: '%s'.\nPlease visit the authorization URL manually.", err)
}
decoded, err := base64.RawStdEncoding.DecodeString(input)
if err != nil {
return nil, err
}

// Start local server
go func() {
err := http.Serve(listener, nil)
if err != nil && err != http.ErrServerClosed {
doneCh <- loginResp{nil, err}
split := strings.Split(string(decoded), "|")
if len(split) != 2 {
return nil, errors.New("failed to decode verification code")
}
}()

code := split[0]
state := split[1]

secret, err := h.codeHandler(c, mount, clientNonce, code, state, doneCh)
go func() {
doneCh <- loginResp{secret, err}
}()
} else {
authURL, clientNonce, err := fetchAuthURL(c, role, mount, callbackPort, callbackMethod, callbackHost, "oidc/callback")
if err != nil {
return nil, err
}

// Set up callback handler
http.HandleFunc("/oidc/callback", callbackHandler(c, mount, clientNonce, doneCh))

listener, err := net.Listen("tcp", listenAddress+":"+port)
if err != nil {
return nil, err
}
defer listener.Close()

// Open the default browser to the callback URL.
fmt.Fprintf(os.Stderr, "Complete the login via your OIDC provider. Launching browser to:\n\n %s\n\n\n", authURL)
if err := openURL(authURL); err != nil {
fmt.Fprintf(os.Stderr, "Error attempting to automatically open browser: '%s'.\nPlease visit the authorization URL manually.", err)
}

// Start local server
go func() {
err := http.Serve(listener, nil)
if err != nil && err != http.ErrServerClosed {
doneCh <- loginResp{nil, err}
}
}()
}

// Wait for either the callback to finish, SIGINT to be received or up to 2 minutes
select {
Expand All @@ -113,7 +169,21 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
}
}

func callbackHandler(c *api.Client, mount string, clientNonce string, doneCh chan<- loginResp) http.HandlerFunc {
func (h *CLIHandler) codeHandler(c *api.Client, mount, clientNonce, code, state string, doneCh chan<- loginResp) (*api.Secret, error) {
data := map[string][]string{
"state": {state},
"code": {code},
"client_nonce": {clientNonce},
}

secret, err := c.Logical().ReadWithData(fmt.Sprintf("auth/%s/oidc/callback", mount), data)
if err != nil {
return nil, err
}
return secret, nil
}

func callbackHandler(c *api.Client, mount, clientNonce string, doneCh chan<- loginResp) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
var response string
var secret *api.Secret
Expand Down Expand Up @@ -160,7 +230,7 @@ func callbackHandler(c *api.Client, mount string, clientNonce string, doneCh cha
}
}

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

clientNonce, err := base62.Random(20)
Expand Down
145 changes: 145 additions & 0 deletions cli_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,151 @@ const successHTML = `
</html>
`

func codeHTML(code 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>Vault Authentication Succeeded</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: #ffffff;
border: 1px solid #000000;
margin-bottom: 12px;
padding: 12px 16px 16px 12px;
position: relative;
border-radius: 2px;
font-size: 14px;
}
.message-content {
margin-left: 4px;
}
.message #checkbox {
fill: #6f7682;
}
.message .message-title {
color: #000000;
font-size: 16px;
font-weight: 700;
line-height: 1.25;
}
.message .message-body {
border: 0;
margin-top: 4px;
}
.message p {
font-size: 14px;
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 content">
<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">
Copy this code and paste it into the prompt:
</div>
<p class="message-body">
%s
</p>
</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>
</body>
</html>
`
return fmt.Sprintf(html, code)
}

func errorHTML(summary, detail string) string {
const html = `
<!DOCTYPE html>
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault v1.4.3 h1:5o6Bfa9loNr9qfUiw6SI6AnOhEhNNCpNhSWkYRjq7nQ=
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820 h1:biZidYDDEWnuOI9mXnJre8lwHKhb5ym85aSXk3oz/dc=
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820/go.mod h1:3f12BMfgDGjTsTtIUj+ZKZwSobQpZtYGFIEehOv5z1o=
github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215195600-2ca765f0a500 h1:tiMX2ewq4ble+e2zENzBvaH2dMoFHe80NbnrF5Ir9Kk=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215195600-2ca765f0a500/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820 h1:TmDZ1sS6gU0hFeFlFuyJVUwRPEzifZIHCBeS2WF2uSc=
Expand Down
39 changes: 39 additions & 0 deletions path_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jwtauth

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -104,6 +105,30 @@ func pathOIDC(b *jwtAuthBackend) []*framework.Path {
Callback: b.authURL,
Summary: "Request an authorization URL to start an OIDC login flow.",

// state is cached so don't process OIDC logins on perf standbys
ForwardPerformanceStandby: true,
},
},
},
{
Pattern: `oidc/manual`,
Fields: map[string]*framework.FieldSchema{
"code": {
Type: framework.TypeString,
Required: true,
Description: "The code value returned from the OIDC process",
},
"state": {
Type: framework.TypeString,
Required: true,
Description: "The state value returned from the OIDC process",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.manual,
Summary: "Used for Manual OIDC Authentication. The provided fields are used to create an encoded output that can be provided to a vault client without a display.",

// state is cached so don't process OIDC logins on perf standbys
ForwardPerformanceStandby: true,
},
Expand Down Expand Up @@ -330,6 +355,20 @@ func (b *jwtAuthBackend) pathCallback(ctx context.Context, req *logical.Request,
return resp, nil
}

func (b *jwtAuthBackend) manual(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
resp := &logical.Response{
Data: map[string]interface{}{
logical.HTTPContentType: "text/html",
logical.HTTPStatusCode: http.StatusOK,
},
}

payload := fmt.Sprintf("%s|%s", d.Get("code").(string), d.Get("state").(string))
resp.Data[logical.HTTPRawBody] = []byte(codeHTML(base64.RawStdEncoding.EncodeToString([]byte(payload))))

return resp, nil
}

// authURL returns a URL used for redirection to receive an authorization code.
// This path requires a role name, or that a default_role has been configured.
// Because this endpoint is unauthenticated, the response to invalid or non-OIDC
Expand Down
Loading