Create alias and command for OIDC (#6206)

This commit is contained in:
Jim Kalafut 2019-02-11 13:37:55 -08:00 committed by GitHub
parent 95f9ba363e
commit 5cb9a8a5fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 2044 additions and 264 deletions

View file

@ -27,6 +27,7 @@ import (
credAliCloud "github.com/hashicorp/vault-plugin-auth-alicloud"
credCentrify "github.com/hashicorp/vault-plugin-auth-centrify"
credGcp "github.com/hashicorp/vault-plugin-auth-gcp/plugin"
credOIDC "github.com/hashicorp/vault-plugin-auth-jwt"
credAws "github.com/hashicorp/vault/builtin/credential/aws"
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
@ -177,6 +178,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
"gcp": &credGcp.CLIHandler{},
"github": &credGitHub.CLIHandler{},
"ldap": &credLdap.CLIHandler{},
"oidc": &credOIDC.CLIHandler{},
"okta": &credOkta.CLIHandler{},
"radius": &credUserpass.CLIHandler{
DefaultMount: "radius",

View file

@ -73,6 +73,7 @@ func newRegistry() *registry {
"jwt": credJWT.Factory,
"kubernetes": credKube.Factory,
"ldap": credLdap.Factory,
"oidc": credJWT.Factory,
"okta": credOkta.Factory,
"radius": credRadius.Factory,
"userpass": credUserpass.Factory,

View file

@ -15,12 +15,12 @@
version = "1.1"
[[projects]]
branch = "master"
digest = "1:6bf6d532e503d9526d46e69aff04d11632c8c1e28b847dbd226babc1689aa723"
digest = "1:c47f4964978e211c6e566596ec6246c329912ea92e9bb99c00798bb4564c5b09"
name = "github.com/armon/go-radix"
packages = ["."]
pruneopts = "UT"
revision = "7fddfc383310abc091d79a27f116d30cf0424032"
revision = "1a2de0c21c94309923825da3df33a4381872c795"
version = "v1.0.0"
[[projects]]
digest = "1:f6e5e1bc64c2908167e6aa9a1fe0c084d515132a1c63ad5b6c84036aa06dc0c1"
@ -39,7 +39,7 @@
version = "v1.0.1"
[[projects]]
digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260"
digest = "1:4c0989ca0bcd10799064318923b9bc2db6b4d6338dd75f3f2d86c3511aaaf5cf"
name = "github.com/golang/protobuf"
packages = [
"proto",
@ -49,8 +49,8 @@
"ptypes/timestamp",
]
pruneopts = "UT"
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
version = "v1.2.0"
[[projects]]
branch = "master"
@ -62,106 +62,108 @@
[[projects]]
branch = "master"
digest = "1:d1971637b21871ec2033a44ca87c99c5608a7340cb34ec75fab8d2ab503276c9"
digest = "1:0ade334594e69404d80d9d323445d2297ff8161637f9b2d347cc6973d2d6f05b"
name = "github.com/hashicorp/errwrap"
packages = ["."]
pruneopts = "UT"
revision = "d6c0cd88035724dd42e0f335ae30161c20575ecc"
revision = "8a6fb523712970c966eefc6b39ed2c5e74880354"
[[projects]]
branch = "master"
digest = "1:77cb3be9b21ba7f1a4701e870c84ea8b66e7d74c7c8951c58155fdadae9414ec"
digest = "1:f47d6109c2034cb16bd62b220e18afd5aa9d5a1630fe5d937ad96a4fb7cbb277"
name = "github.com/hashicorp/go-cleanhttp"
packages = ["."]
pruneopts = "UT"
revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d"
revision = "e8ab9daed8d1ddd2d3c4efba338fe2eeae2e4f18"
[[projects]]
branch = "master"
digest = "1:e8d99882caa8c74d68f340ddb9bba3f7e433117ce57c3e52501edfa7e195d2c7"
digest = "1:0876aeb6edb07e20b6b0ce1d346655cb63dbe0a26ccfb47b68a9b7697709777b"
name = "github.com/hashicorp/go-hclog"
packages = ["."]
pruneopts = "UT"
revision = "ff2cf002a8dd750586d91dddd4470c341f981fe1"
revision = "4783caec6f2e5cdd47fab8b2bb47ce2ce5c546b7"
[[projects]]
branch = "master"
digest = "1:2394f5a25132b3868eff44599cc28d44bdd0330806e34c495d754dd052df612b"
digest = "1:2be5a35f0c5b35162c41bb24971e5dcf6ce825403296ee435429cdcc4e1e847e"
name = "github.com/hashicorp/go-immutable-radix"
packages = ["."]
pruneopts = "UT"
revision = "7f3cd4390caab3250a57f30efdb2a65dd7649ecf"
revision = "27df80928bb34bb1b0d6d0e01b9e679902e7a6b5"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:46fb6a9f1b9667f32ac93e08b1da118b2c666991424ea12e848b05d4fe5155ef"
digest = "1:f668349b83f7d779567c880550534addeca7ebadfdcf44b0b9c39be61864b4b7"
name = "github.com/hashicorp/go-multierror"
packages = ["."]
pruneopts = "UT"
revision = "3d5d8f294aa03d8e98859feac328afbdf1ae0703"
revision = "886a7fbe3eb1c874d46f623bfa70af45f425b3d1"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:20f78c1cf1b6fe6c55ba1407350d6fc7dc77d1591f8106ba693c28014a1a1b37"
digest = "1:77a6108b8eb3cd0feac4eeb3e032f36c8fdfe9497671952fd9eb682b9c503158"
name = "github.com/hashicorp/go-plugin"
packages = ["."]
packages = [
".",
"internal/proto",
]
pruneopts = "UT"
revision = "a4620f9913d19f03a6bf19b2f304daaaf83ea130"
revision = "362c99b11937c6a84686ee5726a8170e921ab406"
[[projects]]
branch = "master"
digest = "1:183f00c472fb9b2446659618eebf4899872fa267b92f926539411abdc8b941df"
digest = "1:d260503602063d71718eb21f85c02133ad5eac894c2a6f0e0546b7dc017dc97e"
name = "github.com/hashicorp/go-retryablehttp"
packages = ["."]
pruneopts = "UT"
revision = "e651d75abec6fbd4f2c09508f72ae7af8a8b7171"
revision = "73489d0a1476f0c9e6fb03f9c39241523a496dfd"
version = "v0.5.2"
[[projects]]
branch = "master"
digest = "1:45aad874d3c7d5e8610427c81870fb54970b981692930ec2a319ce4cb89d7a00"
digest = "1:a54ada9beb59fdc35b69322979e870ff0b780e03f4dc309c4c8674b94927df75"
name = "github.com/hashicorp/go-rootcerts"
packages = ["."]
pruneopts = "UT"
revision = "6bb64b370b90e7ef1fa532be9e591a81c3493e00"
revision = "63503fb4e1eca22f9ae0f90b49c5d5538a0e87eb"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:14f2005c31ddf99c4a0f36fc440f8d1ac43224194c7c4a904b3c8f4ba5654d0b"
digest = "1:3c4c27026ab6a3218dbde897568f651c81062e2ee6e617e57ae46ca95bb1db6b"
name = "github.com/hashicorp/go-sockaddr"
packages = ["."]
pruneopts = "UT"
revision = "6d291a969b86c4b633730bfc6b8b9d64c3aafed9"
revision = "3aed17b5ee41761cc2b04f2a94c7107d428967e5"
[[projects]]
branch = "master"
digest = "1:354978aad16c56c27f57e5b152224806d87902e4935da3b03e18263d82ae77aa"
digest = "1:f14364057165381ea296e49f8870a9ffce2b8a95e34d6ae06c759106aaef428c"
name = "github.com/hashicorp/go-uuid"
packages = ["."]
pruneopts = "UT"
revision = "27454136f0364f2d44b1276c552d69105cf8c498"
revision = "4f571afc59f3043a65f8fe6bf46d887b10a01d43"
version = "v1.0.1"
[[projects]]
branch = "master"
digest = "1:32c0e96a63bd093eccf37db757fb314be5996f34de93969321c2cbef893a7bd6"
digest = "1:950caca7dfcf796419232ba996c9c3539d09f26af27ba848c4508e604c13efbb"
name = "github.com/hashicorp/go-version"
packages = ["."]
pruneopts = "UT"
revision = "270f2f71b1ee587f3b609f00f422b76a6b28f348"
revision = "d40cf49b3a77bba84a7afdbd7f1dc295d114efb1"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:cf296baa185baae04a9a7004efee8511d08e2f5f51d4cbe5375da89722d681db"
digest = "1:8ec8d88c248041a6df5f6574b87bc00e7e0b493881dad2e7ef47b11dc69093b5"
name = "github.com/hashicorp/golang-lru"
packages = [
".",
"simplelru",
]
pruneopts = "UT"
revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3"
revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768"
version = "v0.5.0"
[[projects]]
branch = "master"
digest = "1:12247a2e99a060cc692f6680e5272c8adf0b8f572e6bce0d7095e624c958a240"
digest = "1:ea40c24cdbacd054a6ae9de03e62c5f252479b96c716375aace5c120d68647c8"
name = "github.com/hashicorp/hcl"
packages = [
".",
@ -175,11 +177,12 @@
"json/token",
]
pruneopts = "UT"
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:d00de8725219a569ffbb5dd1042e4ced1f3b5ccee2b07218371f71026cc7609a"
digest = "1:f5bdd7b0d06bfa965cefa9c52af7f556bd079ff4328d67c89f6afdf4be7eabbe"
name = "github.com/hashicorp/vault"
packages = [
"api",
@ -187,9 +190,11 @@
"helper/cidrutil",
"helper/compressutil",
"helper/consts",
"helper/cryptoutil",
"helper/errutil",
"helper/hclutil",
"helper/jsonutil",
"helper/license",
"helper/locksutil",
"helper/logging",
"helper/mlock",
@ -209,39 +214,47 @@
"version",
]
pruneopts = "UT"
revision = "8655d167084028d627f687ddc25d0c71307eb5be"
revision = "b16527d791ba46f74a608527b328957618aa0ae6"
[[projects]]
branch = "master"
digest = "1:89658943622e6bc5e76b4da027ee9583fa0b321db0c797bd554edab96c1ca2b1"
digest = "1:a4826c308e84f5f161b90b54a814f0be7d112b80164b9b884698a6903ea47ab3"
name = "github.com/hashicorp/yamux"
packages = ["."]
pruneopts = "UT"
revision = "3520598351bb3500a49ae9563f5539666ae0a27c"
revision = "2f1d1f20f75d5404f53b9edf6b53ed5505508675"
[[projects]]
branch = "master"
digest = "1:c7354463195544b1ab3c1f1fadb41430947f5d28dfbf2cdbd38268c5717a5a03"
digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "UT"
revision = "58046073cbffe2f25d425fe1331102f55cf719de"
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:cae1afe858922bd10e9573b87130f730a6e4183a00eba79920d6656629468bfa"
digest = "1:42eb1f52b84a06820cedc9baec2e710bfbda3ee6dac6cdb97f8b9a5066134ec6"
name = "github.com/mitchellh/go-testing-interface"
packages = ["."]
pruneopts = "UT"
revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
revision = "6d0b8010fcc857872e42fc6c931227569016843c"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:5ab79470a1d0fb19b041a624415612f8236b3c06070161a910562f2b2d064355"
digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "UT"
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe"
version = "v1.1.2"
[[projects]]
branch = "master"
digest = "1:302de3c669b04a566d4e99760d6fb35a22177fc14c7a9284e8b3cf6e9fe3f28a"
name = "github.com/mitchellh/pointerstructure"
packages = ["."]
pruneopts = "UT"
revision = "f2329fcfa9e280bdb5a3f2544aec815a508ad72f"
[[projects]]
digest = "1:9ec6cf1df5ad1d55cf41a43b6b1e7e118a91bade4f68ff4303379343e40c0e25"
@ -251,6 +264,25 @@
revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39"
version = "v1.0.0"
[[projects]]
digest = "1:808cdddf087fb64baeae67b8dfaee2069034d9704923a3cb8bd96a995421a625"
name = "github.com/patrickmn/go-cache"
packages = ["."]
pruneopts = "UT"
revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
version = "v2.1.0"
[[projects]]
digest = "1:c7a5e79396b6eb570159df7a1d487ce5775bf43b7907976fbef6de544ea160ad"
name = "github.com/pierrec/lz4"
packages = [
".",
"internal/xxh32",
]
pruneopts = "UT"
revision = "473cd7ce01a1113208073166464b98819526150e"
version = "v2.0.8"
[[projects]]
branch = "master"
digest = "1:bd9efe4e0b0f768302a1e2f0c22458149278de533e521206e5ddc71848c269a0"
@ -263,28 +295,29 @@
revision = "1555304b9b35fdd2b425bccf1a5613677705e7d0"
[[projects]]
digest = "1:0e792eea6c96ec55ff302ef33886acbaa5006e900fefe82689e88d96439dcd84"
digest = "1:6baa565fe16f8657cf93469b2b8a6c61a277827734400d27e44d589547297279"
name = "github.com/ryanuber/go-glob"
packages = ["."]
pruneopts = "UT"
revision = "572520ed46dbddaed19ea3d9541bdd0494163693"
version = "v0.1"
revision = "51a8f68e6c24dc43f1e371749c89a267de4ebc53"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:b8fa1ff0fc20983395978b3f771bb10438accbfe19326b02e236c1d4bf1c91b2"
digest = "1:5bce6a1c0d1492cef01d74084ddbac09c4bbc4cbc1db3fdd0c138ed9bc945bf8"
name = "golang.org/x/crypto"
packages = [
"blake2b",
"ed25519",
"ed25519/internal/edwards25519",
"pbkdf2",
]
pruneopts = "UT"
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
revision = "193df9c0f06f8bb35fba505183eaf0acc0136505"
[[projects]]
branch = "master"
digest = "1:3c4175c2711d67096567fc2d84a83464d6ff58119af3efc89983339d64144cb0"
digest = "1:9d2f08c64693fbe7177b5980f80c35672c80f12be79bb3bc86948b934d70e4ee"
name = "golang.org/x/net"
packages = [
"context",
@ -297,26 +330,29 @@
"trace",
]
pruneopts = "UT"
revision = "aaf60122140d3fcf75376d319f0554393160eb50"
revision = "65e2d4e15006aab9813ff8769e768bbf4bb667a0"
[[projects]]
branch = "master"
digest = "1:af19f6e6c369bf51ef226e989034cd88a45083173c02ac4d7ab74c9a90d356b7"
digest = "1:e007b54f54cbd4214aa6d97a67d57bc2539991adb4e22ea92c482bbece8de469"
name = "golang.org/x/oauth2"
packages = [
".",
"internal",
]
pruneopts = "UT"
revision = "3d292e4d0cdc3a0113e6d207bb137145ef1de42f"
revision = "99b60b757ec124ebb7d6b7e97f153b19c10ce163"
[[projects]]
branch = "master"
digest = "1:05662433b3a13c921587a6e622b5722072edff83211efd1cd79eeaeedfd83f07"
digest = "1:c9e49928119661a681af4037236af47654d6bd421c0af184962c890d0a61e0fb"
name = "golang.org/x/sys"
packages = ["unix"]
packages = [
"cpu",
"unix",
]
pruneopts = "UT"
revision = "1c9583448a9c3aa0f9a6a5241bf73c0bd8aafded"
revision = "3b5209105503162ded1863c307ac66fec31120dd"
[[projects]]
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
@ -343,14 +379,14 @@
[[projects]]
branch = "master"
digest = "1:c9e7a4b4d47c0ed205d257648b0e5b0440880cb728506e318f8ac7cd36270bc4"
digest = "1:9fdc2b55e8e0fafe4b41884091e51e77344f7dc511c5acedcfd98200003bff90"
name = "golang.org/x/time"
packages = ["rate"]
pruneopts = "UT"
revision = "fbb02b2291d28baffd63558aa44b4b56f178d650"
revision = "85acf8d2951cb2a3bde7632f9ff273ef0379bcbd"
[[projects]]
digest = "1:328b5e4f197d928c444a51a75385f4b978915c0e75521f0ad6a3db976c97a7d3"
digest = "1:6f3bd49ddf2e104e52062774d797714371fac1b8bddfd8e124ce78e6b2264a10"
name = "google.golang.org/appengine"
packages = [
"internal",
@ -362,8 +398,8 @@
"urlfetch",
]
pruneopts = "UT"
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
version = "v1.1.0"
revision = "e9657d882bb81064595ca3b56cbe2546bbabf7b1"
version = "v1.4.0"
[[projects]]
branch = "master"
@ -371,19 +407,21 @@
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
pruneopts = "UT"
revision = "d0a8f471bba2dbb160885b0000d814ee5d559bad"
revision = "4b09977fb92221987e99d190c8f88f2c92727a29"
[[projects]]
digest = "1:047efbc3c9a51f3002b0002f92543857d372654a676fb6b01931982cd80467dd"
digest = "1:a887a56d0ff92cf05b4bb6004b46fc6e64d3fb6aca4eaeb1466bdce183ba5004"
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"binarylog/grpc_binarylog_v1",
"codes",
"connectivity",
"credentials",
"credentials/internal",
"encoding",
"encoding/proto",
"grpclog",
@ -391,9 +429,12 @@
"health/grpc_health_v1",
"internal",
"internal/backoff",
"internal/binarylog",
"internal/channelz",
"internal/envconfig",
"internal/grpcrand",
"internal/grpcsync",
"internal/syscall",
"internal/transport",
"keepalive",
"metadata",
@ -407,11 +448,11 @@
"tap",
]
pruneopts = "UT"
revision = "32fb0ac620c32ba40a4626ddf94d90d12cce3455"
version = "v1.14.0"
revision = "a02b0774206b209466313a0b525d2c738fe407eb"
version = "v1.18.0"
[[projects]]
digest = "1:b57bb9a6a2a03558d63166f1afc3c0c4f91ad137f63bf2bee995e9baeb976a9c"
digest = "1:a4cde1eec9a17eb2399a50c6e1a9fe3fde039994de058f9dbf6592d157bfe97b"
name = "gopkg.in/square/go-jose.v2"
packages = [
".",
@ -420,8 +461,8 @@
"jwt",
]
pruneopts = "UT"
revision = "8254d6c783765f38c8675fae4427a1fe73fbd09d"
version = "v2.1.8"
revision = "e94fb177d3668d35ab39c61cbb2f311550557e83"
version = "v2.2.2"
[solve-meta]
analyzer-name = "dep"
@ -433,6 +474,8 @@
"github.com/hashicorp/go-cleanhttp",
"github.com/hashicorp/go-hclog",
"github.com/hashicorp/go-sockaddr",
"github.com/hashicorp/go-uuid",
"github.com/hashicorp/vault/api",
"github.com/hashicorp/vault/helper/certutil",
"github.com/hashicorp/vault/helper/cidrutil",
"github.com/hashicorp/vault/helper/logging",
@ -443,6 +486,8 @@
"github.com/hashicorp/vault/logical",
"github.com/hashicorp/vault/logical/framework",
"github.com/hashicorp/vault/logical/plugin",
"github.com/mitchellh/pointerstructure",
"github.com/patrickmn/go-cache",
"golang.org/x/oauth2",
"gopkg.in/square/go-jose.v2",
"gopkg.in/square/go-jose.v2/jwt",

View file

@ -3,10 +3,12 @@ package jwtauth
import (
"context"
"sync"
"time"
oidc "github.com/coreos/go-oidc"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
cache "github.com/patrickmn/go-cache"
)
const (
@ -16,7 +18,7 @@ const (
// Factory is used by framework
func Factory(ctx context.Context, c *logical.BackendConfig) (logical.Backend, error) {
b := backend(c)
b := backend()
if err := b.Setup(ctx, c); err != nil {
return nil, err
}
@ -29,14 +31,16 @@ type jwtAuthBackend struct {
l sync.RWMutex
provider *oidc.Provider
cachedConfig *jwtConfig
oidcStates *cache.Cache
providerCtx context.Context
providerCtxCancel context.CancelFunc
}
func backend(c *logical.BackendConfig) *jwtAuthBackend {
func backend() *jwtAuthBackend {
b := new(jwtAuthBackend)
b.providerCtx, b.providerCtxCancel = context.WithCancel(context.Background())
b.oidcStates = cache.New(oidcStateTimeout, 1*time.Minute)
b.Backend = &framework.Backend{
AuthRenew: b.pathLoginRenew,
@ -46,6 +50,9 @@ func backend(c *logical.BackendConfig) *jwtAuthBackend {
PathsSpecial: &logical.Paths{
Unauthenticated: []string{
"login",
"oidc/auth_url",
"oidc/callback",
"ui", // TODO: remove when Vault UI is ready
},
SealWrapStorage: []string{
"config",
@ -57,7 +64,9 @@ func backend(c *logical.BackendConfig) *jwtAuthBackend {
pathRoleList(b),
pathRole(b),
pathConfig(b),
pathUI(b), // TODO: remove when Vault UI is ready
},
pathOIDC(b),
),
Clean: b.cleanup,
}

View file

@ -0,0 +1,65 @@
package jwtauth
import (
"fmt"
"strings"
log "github.com/hashicorp/go-hclog"
"github.com/mitchellh/pointerstructure"
)
// getClaim returns a claim value from allClaims given a provided claim string.
// If this string is a valid JSONPointer, it will be interpreted as such to locate
// the claim. Otherwise, the claim string will be used directly.
func getClaim(logger log.Logger, allClaims map[string]interface{}, claim string) interface{} {
var val interface{}
var err error
if !strings.HasPrefix(claim, "/") {
val = allClaims[claim]
} else {
val, err = pointerstructure.Get(allClaims, claim)
if err != nil {
logger.Warn(fmt.Sprintf("unable to locate %s in claims: %s", claim, err.Error()))
return nil
}
}
// The claims unmarshalled by go-oidc don't use UseNumber, so there will
// be mismatches if they're coming in as float64 since Vault's config will
// be represented as json.Number. If the operator can coerce claims data to
// be in string form, there is no problem. Alternatively, we could try to
// intelligently convert float64 to json.Number, e.g.:
//
// switch v := val.(type) {
// case float64:
// val = json.Number(strconv.Itoa(int(v)))
// }
//
// Or we fork and/or PR go-oidc.
return val
}
// extractMetadata builds a metadata map from a set of claims and claims mappings.
// The referenced claims must be strings and the claims mappings must be of the structure:
//
// {
// "/some/claim/pointer": "metadata_key1",
// "another_claim": "metadata_key2",
// ...
// }
func extractMetadata(logger log.Logger, allClaims map[string]interface{}, claimMappings map[string]string) (map[string]string, error) {
metadata := make(map[string]string)
for source, target := range claimMappings {
if value := getClaim(logger, allClaims, source); value != nil {
strValue, ok := value.(string)
if !ok {
return nil, fmt.Errorf("error converting claim '%s' to string", source)
}
metadata[target] = strValue
}
}
return metadata, nil
}

View file

@ -0,0 +1,502 @@
package jwtauth
import (
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"os/signal"
"regexp"
"runtime"
"strings"
"github.com/hashicorp/vault/api"
)
const defaultMount = "oidc"
const defaultPort = "8300"
type CLIHandler struct{}
type loginResp struct {
secret *api.Secret
err error
}
func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) {
// handle ctrl-c while waiting for the callback
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
defer signal.Stop(ch)
doneCh := make(chan loginResp)
mount, ok := m["mount"]
if !ok {
mount = defaultMount
}
port, ok := m["port"]
if !ok {
port = defaultPort
}
role := m["role"]
if role == "" {
return nil, errors.New("a 'role' must be specified")
}
secret, err := fetchAuthURL(c, role, mount, port)
if err != nil {
return nil, err
}
authURL := secret.Data["auth_url"].(string)
if authURL == "" {
return nil, errors.New(fmt.Sprintf("Unable to authorize role %q. Check Vault logs for more information.", role))
}
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)
}
// Set up callback handler
http.HandleFunc(fmt.Sprintf("/v1/auth/%s/oidc/callback", mount), 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}
})
// Start local server
go func() {
if err := http.ListenAndServe(":"+port, nil); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "Error listening for callback: %v\n\n", err.Error())
}
}()
// Wait for either the callback to finish or SIGINT to be received
select {
case s := <-doneCh:
return s.secret, s.err
case <-ch:
return nil, errors.New("interrupted")
}
}
func fetchAuthURL(c *api.Client, role, mount, port string) (*api.Secret, error) {
data := map[string]interface{}{
"role": role,
"redirect_uri": fmt.Sprintf("http://localhost:%s/v1/auth/%s/oidc/callback", port, mount),
}
return c.Logical().Write(fmt.Sprintf("auth/%s/oidc/auth_url", mount), data)
}
// openURL opens the specified URL in the default browser of the user.
// Source: https://stackoverflow.com/a/39324149/453290
func openURL(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
// parseError converts error from the API into summary and detailed portions.
func parseError(err error) (string, string) {
headers := []string{errNoResponse, errLoginFailed, errTokenVerification}
summary := "Login error"
detail := ""
re := regexp.MustCompile(`(?s)Errors:.*\* *(.*)`)
errorParts := re.FindStringSubmatch(err.Error())
switch len(errorParts) {
case 0:
summary = ""
case 1:
detail = errorParts[0]
case 2:
for _, h := range headers {
if strings.HasPrefix(errorParts[1], h) {
summary = h
detail = strings.TrimSpace(errorParts[1][len(h):])
break
}
}
if detail == "" {
detail = errorParts[1]
}
}
return summary, detail
}
func errorHTML(summary, detail 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>HashiCorp Vault</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.is-danger {
background: #fdfafb;
border-color: #f9ecee;
}
.message-content {
margin-left: 4px;
}
.message svg {
fill: #2eb039;
}
.message.is-danger svg {
fill: #c73445;
}
.message .message-title {
color: #1e7125;
font-size: 16px;
font-weight: 700;
line-height: 1.25;
}
.message.is-danger .message-title {
color: #7f222c;
}
.message .message-body {
border: 0;
margin-top: 4px;
}
.message p {
font-size: 12px;
margin: 0;
padding: 0;
color: #17421b;
}
.message.is-danger p {
color: #1f2124;
}
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-danger">
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M19 3c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h14zm-2 12.59L13.41 12 17 8.41 15.59 7 12 10.59 8.41 7 7 8.41 10.59 12 7 15.59 8.41 17 12 13.41 15.59 17 17 15.59z"></path>
</svg>
<div class="message-content">
<div class="message-title">
%s
</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, summary, detail)
}
// Help method for OIDC cli
func (h *CLIHandler) Help() string {
help := `
Usage: vault login -method=oidc [CONFIG K=V...]
The OIDC auth method allows users to authenticate using an OIDC provider.
The provider must be configured as part of a role by the operator.
Authenticate using role "engineering":
$ vault login -method=oidc role=engineering
Complete the login via your OIDC provider. Launching browser to:
https://accounts.google.com/o/oauth2/v2/...
The default browser will be opened for the user to complete the login. Alternatively,
the user may visit the provided URL directly.
Configuration:
role=<string>
Vault role of type "OIDC" to use for authentication.
port=<string>
Optional localhost port to use for OIDC callback (default: 8300).
`
return strings.TrimSpace(help)
}
const successHTML = `
<!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: #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">
Signed in via your OIDC provider
</div>
<p class="message-body">
You can now close this window and start using Vault.
</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>
`

View file

@ -4,6 +4,7 @@ import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
"context"
@ -29,19 +30,44 @@ func pathConfig(b *jwtAuthBackend) *framework.Path {
Type: framework.TypeString,
Description: "The CA certificate or chain of certificates, in PEM format, to use to validate conections to the OIDC Discovery URL. If not set, system certificates are used.",
},
"oidc_client_id": {
Type: framework.TypeString,
Description: "The OAuth Client ID configured with your OIDC provider.",
},
"oidc_client_secret": {
Type: framework.TypeString,
Description: "The OAuth Client Secret configured with your OIDC provider.",
DisplaySensitive: true,
},
"default_role": {
Type: framework.TypeString,
Description: "The default role to use if none is provided during login. If not set, a role is required during login.",
},
"jwt_validation_pubkeys": {
Type: framework.TypeCommaStringSlice,
Description: `A list of PEM-encoded public keys to use to authenticate signatures locally. Cannot be used with "oidc_discovery_url".`,
},
"jwt_supported_algs": {
Type: framework.TypeCommaStringSlice,
Description: `A list of supported signing algorithms. Defaults to RS256.`,
},
"bound_issuer": {
Type: framework.TypeString,
Description: "The value against which to match the 'iss' claim in a JWT. Optional.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathConfigRead,
logical.UpdateOperation: b.pathConfigWrite,
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathConfigRead,
Summary: "Read the current JWT authentication backend configuration.",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathConfigWrite,
Summary: "Configure the JWT authentication backend.",
Description: confHelpDesc,
},
},
HelpSynopsis: confHelpSyn,
@ -98,7 +124,11 @@ func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques
Data: map[string]interface{}{
"oidc_discovery_url": config.OIDCDiscoveryURL,
"oidc_discovery_ca_pem": config.OIDCDiscoveryCAPEM,
"oidc_client_id": config.OIDCClientID,
"oidc_client_secret": config.OIDCClientSecret,
"default_role": config.DefaultRole,
"jwt_validation_pubkeys": config.JWTValidationPubKeys,
"jwt_supported_algs": config.JWTSupportedAlgs,
"bound_issuer": config.BoundIssuer,
},
}
@ -110,7 +140,11 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
config := &jwtConfig{
OIDCDiscoveryURL: d.Get("oidc_discovery_url").(string),
OIDCDiscoveryCAPEM: d.Get("oidc_discovery_ca_pem").(string),
OIDCClientID: d.Get("oidc_client_id").(string),
OIDCClientSecret: d.Get("oidc_client_secret").(string),
DefaultRole: d.Get("default_role").(string),
JWTValidationPubKeys: d.Get("jwt_validation_pubkeys").([]string),
JWTSupportedAlgs: d.Get("jwt_supported_algs").([]string),
BoundIssuer: d.Get("bound_issuer").(string),
}
@ -120,12 +154,19 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
config.OIDCDiscoveryURL != "" && len(config.JWTValidationPubKeys) != 0:
return logical.ErrorResponse("exactly one of 'oidc_discovery_url' and 'jwt_validation_pubkeys' must be set"), nil
case config.OIDCClientID != "" && config.OIDCClientSecret == "",
config.OIDCClientID == "" && config.OIDCClientSecret != "":
return logical.ErrorResponse("both 'oidc_client_id' and 'oidc_client_secret' must be set for OIDC"), nil
case config.OIDCDiscoveryURL != "":
_, err := b.createProvider(config)
if err != nil {
return logical.ErrorResponse(errwrap.Wrapf("error checking discovery URL: {{err}}", err).Error()), nil
}
case config.OIDCClientID != "" && config.OIDCDiscoveryURL == "":
return logical.ErrorResponse("'oidc_discovery_url' must be set for OIDC"), nil
case len(config.JWTValidationPubKeys) != 0:
for _, v := range config.JWTValidationPubKeys {
if _, err := certutil.ParsePublicKeyPEM([]byte(v)); err != nil {
@ -137,6 +178,14 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
return nil, errors.New("unknown condition")
}
for _, a := range config.JWTSupportedAlgs {
switch a {
case oidc.RS256, oidc.RS384, oidc.RS512, oidc.ES256, oidc.ES384, oidc.ES512, oidc.PS256, oidc.PS384, oidc.PS512:
default:
return logical.ErrorResponse(fmt.Sprintf("Invalid supported algorithm: %s", a)), nil
}
}
entry, err := logical.StorageEntryJSON(configPath, config)
if err != nil {
return nil, err
@ -181,8 +230,12 @@ func (b *jwtAuthBackend) createProvider(config *jwtConfig) (*oidc.Provider, erro
type jwtConfig struct {
OIDCDiscoveryURL string `json:"oidc_discovery_url"`
OIDCDiscoveryCAPEM string `json:"oidc_discovery_ca_pem"`
OIDCClientID string `json:"oidc_client_id"`
OIDCClientSecret string `json:"oidc_client_secret"`
JWTValidationPubKeys []string `json:"jwt_validation_pubkeys"`
JWTSupportedAlgs []string `json:"jwt_supported_algs"`
BoundIssuer string `json:"bound_issuer"`
DefaultRole string `json:"default_role"`
ParsedJWTPubKeys []interface{} `json:"-"`
}

View file

@ -29,9 +29,14 @@ func pathLogin(b *jwtAuthBackend) *framework.Path {
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathLogin,
logical.AliasLookaheadOperation: b.pathLogin,
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathLogin,
Summary: pathLoginHelpSyn,
},
logical.AliasLookaheadOperation: &framework.PathOperation{
Callback: b.pathLogin,
},
},
HelpSynopsis: pathLoginHelpSyn,
@ -40,13 +45,19 @@ func pathLogin(b *jwtAuthBackend) *framework.Path {
}
func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
token := d.Get("jwt").(string)
if len(token) == 0 {
return logical.ErrorResponse("missing token"), nil
config, err := b.config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return logical.ErrorResponse("could not load configuration"), nil
}
roleName := d.Get("role").(string)
if len(roleName) == 0 {
if roleName == "" {
roleName = config.DefaultRole
}
if roleName == "" {
return logical.ErrorResponse("missing role"), nil
}
@ -55,21 +66,18 @@ func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
return nil, err
}
if role == nil {
return logical.ErrorResponse("role could not be found"), nil
return logical.ErrorResponse("role %q could not be found", roleName), nil
}
token := d.Get("jwt").(string)
if len(token) == 0 {
return logical.ErrorResponse("missing token"), nil
}
if req.Connection != nil && !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, role.BoundCIDRs) {
return logical.ErrorResponse("request originated from invalid CIDR"), nil
}
config, err := b.config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return logical.ErrorResponse("could not load configuration"), nil
}
// Here is where things diverge. If it is using OIDC Discovery, validate
// that way; otherwise validate against the locally configured keys. Once
// things are validated, we re-unify the request path when evaluating the
@ -130,118 +138,37 @@ func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
}
case config.OIDCDiscoveryURL != "":
provider, err := b.getProvider(ctx, config)
allClaims, err = b.verifyToken(ctx, config, role, token)
if err != nil {
return nil, errwrap.Wrapf("error getting provider for login operation: {{err}}", err)
}
verifier := provider.Verifier(&oidc.Config{
SkipClientIDCheck: true,
})
idToken, err := verifier.Verify(ctx, token)
if err != nil {
return logical.ErrorResponse(errwrap.Wrapf("error validating signature: {{err}}", err).Error()), nil
}
if err := idToken.Claims(&allClaims); err != nil {
return logical.ErrorResponse(errwrap.Wrapf("unable to successfully parse all claims from token: {{err}}", err).Error()), nil
}
if role.BoundSubject != "" && role.BoundSubject != idToken.Subject {
return logical.ErrorResponse("sub claim does not match bound subject"), nil
}
if len(role.BoundAudiences) != 0 {
var found bool
for _, v := range role.BoundAudiences {
if strutil.StrListContains(idToken.Audience, v) {
found = true
break
}
}
if !found {
return logical.ErrorResponse("aud claim does not match any bound audience"), nil
}
return logical.ErrorResponse(err.Error()), nil
}
default:
return nil, errors.New("unhandled case during login")
}
userClaimRaw, ok := allClaims[role.UserClaim]
if !ok {
return logical.ErrorResponse(fmt.Sprintf("%q claim not found in token", role.UserClaim)), nil
}
userName, ok := userClaimRaw.(string)
if !ok {
return logical.ErrorResponse(fmt.Sprintf("%q claim could not be converted to string", role.UserClaim)), nil
alias, groupAliases, err := b.createIdentity(allClaims, role)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
var groupAliases []*logical.Alias
if role.GroupsClaim != "" {
mapPath, err := parseClaimWithDelimiters(role.GroupsClaim, role.GroupsClaimDelimiterPattern)
if err != nil {
return logical.ErrorResponse(errwrap.Wrapf("error parsing delimiters for groups claim: {{err}}", err).Error()), nil
}
if len(mapPath) < 1 {
return logical.ErrorResponse("unexpected length 0 of claims path after parsing groups claim against delimiters"), nil
}
var claimKey string
claimMap := allClaims
for i, key := range mapPath {
if i == len(mapPath)-1 {
claimKey = key
break
}
nextMapRaw, ok := claimMap[key]
if !ok {
return logical.ErrorResponse(fmt.Sprintf("map via key %q not found while navigating group claim delimiters", key)), nil
}
nextMap, ok := nextMapRaw.(map[string]interface{})
if !ok {
return logical.ErrorResponse(fmt.Sprintf("key %q does not reference a map while navigating group claim delimiters", key)), nil
}
claimMap = nextMap
}
groupsClaimRaw, ok := claimMap[claimKey]
if !ok {
return logical.ErrorResponse(fmt.Sprintf("%q claim not found in token", role.GroupsClaim)), nil
}
groups, ok := groupsClaimRaw.([]interface{})
if !ok {
return logical.ErrorResponse(fmt.Sprintf("%q claim could not be converted to string list", role.GroupsClaim)), nil
}
for _, groupRaw := range groups {
group, ok := groupRaw.(string)
if !ok {
return logical.ErrorResponse(fmt.Sprintf("value %v in groups claim could not be parsed as string", groupRaw)), nil
}
if group == "" {
continue
}
groupAliases = append(groupAliases, &logical.Alias{
Name: group,
})
}
tokenMetadata := map[string]string{"role": roleName}
for k, v := range alias.Metadata {
tokenMetadata[k] = v
}
resp := &logical.Response{
Auth: &logical.Auth{
Policies: role.Policies,
DisplayName: userName,
Period: role.Period,
NumUses: role.NumUses,
Alias: &logical.Alias{
Name: userName,
},
Policies: role.Policies,
DisplayName: alias.Name,
Period: role.Period,
NumUses: role.NumUses,
Alias: alias,
GroupAliases: groupAliases,
InternalData: map[string]interface{}{
"role": roleName,
},
Metadata: map[string]string{
"role": roleName,
},
Metadata: tokenMetadata,
LeaseOptions: logical.LeaseOptions{
Renewable: true,
TTL: role.TTL,
@ -276,6 +203,120 @@ func (b *jwtAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Reques
return resp, nil
}
func (b *jwtAuthBackend) verifyToken(ctx context.Context, config *jwtConfig, role *jwtRole, rawToken string) (map[string]interface{}, error) {
allClaims := make(map[string]interface{})
provider, err := b.getProvider(ctx, config)
if err != nil {
return nil, errwrap.Wrapf("error getting provider for login operation: {{err}}", err)
}
oidcConfig := &oidc.Config{
SupportedSigningAlgs: config.JWTSupportedAlgs,
}
if role.RoleType == "oidc" {
oidcConfig.ClientID = config.OIDCClientID
} else {
oidcConfig.SkipClientIDCheck = true
}
verifier := provider.Verifier(oidcConfig)
idToken, err := verifier.Verify(ctx, rawToken)
if err != nil {
return nil, errwrap.Wrapf("error validating signature: {{err}}", err)
}
if err := idToken.Claims(&allClaims); err != nil {
return nil, errwrap.Wrapf("unable to successfully parse all claims from token: {{err}}", err)
}
if role.BoundSubject != "" && role.BoundSubject != idToken.Subject {
return nil, errors.New("sub claim does not match bound subject")
}
if len(role.BoundAudiences) > 0 {
var found bool
for _, v := range role.BoundAudiences {
if strutil.StrListContains(idToken.Audience, v) {
found = true
break
}
}
if !found {
return nil, errors.New("aud claim does not match any bound audience")
}
}
if len(role.BoundClaims) > 0 {
for claim, expValue := range role.BoundClaims {
actValue := getClaim(b.Logger(), allClaims, claim)
if actValue == nil {
return nil, fmt.Errorf("claim is missing: %s", claim)
}
if expValue != actValue {
return nil, fmt.Errorf("claim '%s' does not match associated bound claim", claim)
}
}
}
return allClaims, nil
}
// createIdentity creates an alias and set of groups aliass based on the role
// definition and received claims.
func (b *jwtAuthBackend) createIdentity(allClaims map[string]interface{}, role *jwtRole) (*logical.Alias, []*logical.Alias, error) {
userClaimRaw, ok := allClaims[role.UserClaim]
if !ok {
return nil, nil, fmt.Errorf("claim %q not found in token", role.UserClaim)
}
userName, ok := userClaimRaw.(string)
if !ok {
return nil, nil, fmt.Errorf("claim %q could not be converted to string", role.UserClaim)
}
metadata, err := extractMetadata(b.Logger(), allClaims, role.ClaimMappings)
if err != nil {
return nil, nil, err
}
alias := &logical.Alias{
Name: userName,
Metadata: metadata,
}
var groupAliases []*logical.Alias
if role.GroupsClaim == "" {
return alias, groupAliases, nil
}
groupsClaimRaw := getClaim(b.Logger(), allClaims, role.GroupsClaim)
if groupsClaimRaw == nil {
return nil, nil, fmt.Errorf("%q claim not found in token", role.GroupsClaim)
}
groups, ok := groupsClaimRaw.([]interface{})
if !ok {
return nil, nil, fmt.Errorf("%q claim could not be converted to string list", role.GroupsClaim)
}
for _, groupRaw := range groups {
group, ok := groupRaw.(string)
if !ok {
return nil, nil, fmt.Errorf("value %v in groups claim could not be parsed as string", groupRaw)
}
if group == "" {
continue
}
groupAliases = append(groupAliases, &logical.Alias{
Name: group,
})
}
return alias, groupAliases, nil
}
const (
pathLoginHelpSyn = `
Authenticates to Vault using a JWT (or OIDC) token.

View file

@ -0,0 +1,303 @@
package jwtauth
import (
"context"
"fmt"
"strings"
"time"
oidc "github.com/coreos/go-oidc"
"github.com/hashicorp/errwrap"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
"golang.org/x/oauth2"
)
var oidcStateTimeout = 10 * time.Minute
// OIDC error prefixes. This are searched for specifically by the UI, so any
// changes to them must be aligned with a UI change.
const errLoginFailed = "Vault login failed."
const errNoResponse = "No response from provider."
const errTokenVerification = "Token verification failed."
// oidcState is created when an authURL is requested. The state identifier is
// passed throughout the OAuth process.
type oidcState struct {
rolename string
nonce string
redirectURI string
}
func pathOIDC(b *jwtAuthBackend) []*framework.Path {
return []*framework.Path{
{
Pattern: `oidc/callback`,
Fields: map[string]*framework.FieldSchema{
"state": {
Type: framework.TypeString,
},
"code": {
Type: framework.TypeString,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathCallback,
Summary: "Callback endpoint to complete an OIDC login.",
},
},
},
{
Pattern: `oidc/auth_url`,
Fields: map[string]*framework.FieldSchema{
"role": {
Type: framework.TypeLowerCaseString,
Description: "The role to issue an OIDC authorization URL against.",
},
"redirect_uri": {
Type: framework.TypeString,
Description: "The OAuth redirect_uri to use in the authorization URL.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.authURL,
Summary: "Request an authorization URL to start an OIDC login flow.",
},
},
},
}
}
func (b *jwtAuthBackend) pathCallback(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
state := b.verifyState(d.Get("state").(string))
if state == nil {
return logical.ErrorResponse(errLoginFailed + " Expired or missing OAuth state."), nil
}
roleName := state.rolename
role, err := b.role(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse(errLoginFailed + " Role could not be found"), nil
}
config, err := b.config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return logical.ErrorResponse(errLoginFailed + " Could not load configuration"), nil
}
provider, err := b.getProvider(ctx, config)
if err != nil {
return nil, errwrap.Wrapf(errLoginFailed+" Error getting provider for login operation: {{err}}", err)
}
var oauth2Config = oauth2.Config{
ClientID: config.OIDCClientID,
ClientSecret: config.OIDCClientSecret,
RedirectURL: state.redirectURI,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID},
}
code := d.Get("code").(string)
if code == "" {
return logical.ErrorResponse(errLoginFailed + " OAuth code parameter not provided"), nil
}
oauth2Token, err := oauth2Config.Exchange(ctx, code)
if err != nil {
return logical.ErrorResponse(errLoginFailed+" Error exchanging oidc code: %q.", err.Error()), nil
}
// Extract the ID Token from OAuth2 token.
rawToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return logical.ErrorResponse(errTokenVerification + " No id_token found in response."), nil
}
// Parse and verify ID Token payload.
allClaims, err := b.verifyToken(ctx, config, role, rawToken)
if err != nil {
return logical.ErrorResponse("%s %s", errTokenVerification, err.Error()), nil
}
// Attempt to fetch information from the /userinfo endpoint and merge it with
// the existing claims data. A failure to fetch additional information from this
// endpoint will not invalidate the authorization flow.
if userinfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)); err == nil {
_ = userinfo.Claims(&allClaims)
} else {
logFunc := b.Logger().Warn
if strings.Contains(err.Error(), "user info endpoint is not supported") {
logFunc = b.Logger().Info
}
logFunc("error reading /userinfo endpoint", "error", err)
}
if allClaims["nonce"] != state.nonce {
return logical.ErrorResponse(errTokenVerification + " Invalid ID token nonce."), nil
}
delete(allClaims, "nonce")
alias, groupAliases, err := b.createIdentity(allClaims, role)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
tokenMetadata := map[string]string{"role": roleName}
for k, v := range alias.Metadata {
tokenMetadata[k] = v
}
resp := &logical.Response{
Auth: &logical.Auth{
Policies: role.Policies,
DisplayName: alias.Name,
Period: role.Period,
NumUses: role.NumUses,
Alias: alias,
GroupAliases: groupAliases,
InternalData: map[string]interface{}{
"role": roleName,
},
Metadata: tokenMetadata,
LeaseOptions: logical.LeaseOptions{
Renewable: true,
TTL: role.TTL,
MaxTTL: role.MaxTTL,
},
BoundCIDRs: role.BoundCIDRs,
},
}
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
// roles is intentionally non-descriptive and will simply be an empty string.
func (b *jwtAuthBackend) authURL(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
logger := b.Logger()
// default response for most error/invalid conditions
resp := &logical.Response{
Data: map[string]interface{}{
"auth_url": "",
},
}
config, err := b.config(ctx, req.Storage)
if err != nil {
logger.Warn("error loading configuration", "error", err)
return resp, nil
}
if config == nil {
logger.Warn("nil configuration")
return resp, nil
}
roleName := d.Get("role").(string)
if roleName == "" {
roleName = config.DefaultRole
if roleName == "" {
return logical.ErrorResponse("missing role"), nil
}
}
redirectURI := d.Get("redirect_uri").(string)
if redirectURI == "" {
return logical.ErrorResponse("missing redirect_uri"), nil
}
role, err := b.role(ctx, req.Storage, roleName)
if err != nil {
logger.Warn("error loading role", "error", err)
return resp, nil
}
if role == nil || role.RoleType != "oidc" {
logger.Warn("invalid role type", "role type", role)
return resp, nil
}
if !strutil.StrListContains(role.AllowedRedirectURIs, redirectURI) {
logger.Warn("unauthorized redirect_uri", "redirect_uri", redirectURI)
return resp, nil
}
provider, err := b.getProvider(ctx, config)
if err != nil {
logger.Warn("error getting provider for login operation", "error", err)
return resp, nil
}
// "openid" is a required scope for OpenID Connect flows
scopes := append([]string{oidc.ScopeOpenID}, role.OIDCScopes...)
// Configure an OpenID Connect aware OAuth2 client
oauth2Config := oauth2.Config{
ClientID: config.OIDCClientID,
ClientSecret: config.OIDCClientSecret,
RedirectURL: redirectURI,
Endpoint: provider.Endpoint(),
Scopes: scopes,
}
stateID, nonce, err := b.createState(roleName, redirectURI)
if err != nil {
logger.Warn("error generating OAuth state", "error", err)
return resp, nil
}
resp.Data["auth_url"] = oauth2Config.AuthCodeURL(stateID, oidc.Nonce(nonce))
return resp, nil
}
// createState make an expiring state object, associated with a random state ID
// that is passed throughout the OAuth process. A nonce is also included in the
// auth process, and for simplicity will be identical in length/format as the state ID.
func (b *jwtAuthBackend) createState(rolename, redirectURI string) (string, string, error) {
// Get enough bytes for 2 160-bit IDs (per rfc6749#section-10.10)
bytes, err := uuid.GenerateRandomBytes(2 * 20)
if err != nil {
return "", "", err
}
stateID := fmt.Sprintf("%x", bytes[:20])
nonce := fmt.Sprintf("%x", bytes[20:])
b.oidcStates.SetDefault(stateID, &oidcState{
rolename: rolename,
nonce: nonce,
redirectURI: redirectURI,
})
return stateID, nonce, nil
}
// verifyState tests whether the provided state ID is valid and returns the
// associated state object if so. A nil state is returned if the ID is not found
// or expired. The state should only ever be retrieved once and is deleted as
// part of this request.
func (b *jwtAuthBackend) verifyState(stateID string) *oidcState {
defer b.oidcStates.Delete(stateID)
if stateRaw, ok := b.oidcStates.Get(stateID); ok {
return stateRaw.(*oidcState)
}
return nil
}

View file

@ -7,19 +7,25 @@ import (
"strings"
"time"
"github.com/hashicorp/errwrap"
sockaddr "github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
var reservedMetadata = []string{"role"}
func pathRoleList(b *jwtAuthBackend) *framework.Path {
return &framework.Path{
Pattern: "role/?",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathRoleList,
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: b.pathRoleList,
Summary: strings.TrimSpace(roleHelp["role-list"][0]),
Description: strings.TrimSpace(roleHelp["role-list"][1]),
},
},
HelpSynopsis: strings.TrimSpace(roleHelp["role-list"][0]),
HelpDescription: strings.TrimSpace(roleHelp["role-list"][1]),
@ -35,6 +41,10 @@ func pathRole(b *jwtAuthBackend) *framework.Path {
Type: framework.TypeLowerCaseString,
Description: "Name of the role.",
},
"role_type": {
Type: framework.TypeString,
Description: "Type of the role, either 'jwt' or 'oidc'.",
},
"policies": {
Type: framework.TypeCommaStringSlice,
Description: "List of policies on the role.",
@ -68,6 +78,14 @@ TTL will be set to the value of this parameter.`,
Type: framework.TypeCommaStringSlice,
Description: `Comma-separated list of 'aud' claims that are valid for login; any match is sufficient`,
},
"bound_claims": {
Type: framework.TypeMap,
Description: `Map of claims/values which must match for login`,
},
"claim_mappings": {
Type: framework.TypeKVPairs,
Description: `Mappings of claims (key) that will be copied to a metadata field (value)`,
},
"user_claim": {
Type: framework.TypeString,
Description: `The claim to use for the Identity entity alias name`,
@ -76,22 +94,43 @@ TTL will be set to the value of this parameter.`,
Type: framework.TypeString,
Description: `The claim to use for the Identity group alias names`,
},
"groups_claim_delimiter_pattern": {
Type: framework.TypeString,
Description: `A pattern of delimiters used to allow the groups_claim to live outside of the top-level JWT structure. For instance, a "groups_claim" of "meta/user.name/groups" with this field set to "//" will expect nested structures named "meta", "user.name", and "groups". If this field was set to "/./" the groups information would expect to be via nested structures of "meta", "user", "name", and "groups".`,
},
"bound_cidrs": {
Type: framework.TypeCommaStringSlice,
Description: `Comma-separated list of IP CIDRS that are allowed to
authenticate against this role`,
},
"oidc_scopes": {
Type: framework.TypeCommaStringSlice,
Description: `Comma-separated list of OIDC scopes`,
},
"allowed_redirect_uris": {
Type: framework.TypeCommaStringSlice,
Description: `Comma-separated list of allowed values for redirect_uri`,
},
},
ExistenceCheck: b.pathRoleExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathRoleCreateUpdate,
logical.UpdateOperation: b.pathRoleCreateUpdate,
logical.ReadOperation: b.pathRoleRead,
logical.DeleteOperation: b.pathRoleDelete,
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathRoleRead,
Summary: "Read an existing role.",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathRoleCreateUpdate,
Summary: strings.TrimSpace(roleHelp["role"][0]),
Description: strings.TrimSpace(roleHelp["role"][1]),
},
logical.CreateOperation: &framework.PathOperation{
Callback: b.pathRoleCreateUpdate,
Summary: strings.TrimSpace(roleHelp["role"][0]),
Description: strings.TrimSpace(roleHelp["role"][1]),
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.pathRoleDelete,
Summary: "Delete an existing role.",
},
},
HelpSynopsis: strings.TrimSpace(roleHelp["role"][0]),
HelpDescription: strings.TrimSpace(roleHelp["role"][1]),
@ -99,6 +138,8 @@ authenticate against this role`,
}
type jwtRole struct {
RoleType string `json:"role_type"`
// Policies that are to be required by the token to access this role
Policies []string `json:"policies"`
@ -119,12 +160,15 @@ type jwtRole struct {
Period time.Duration `json:"period"`
// Role binding properties
BoundAudiences []string `json:"bound_audiences"`
BoundSubject string `json:"bound_subject"`
BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"`
UserClaim string `json:"user_claim"`
GroupsClaim string `json:"groups_claim"`
GroupsClaimDelimiterPattern string `json:"groups_claim_delimiter_pattern"`
BoundAudiences []string `json:"bound_audiences"`
BoundSubject string `json:"bound_subject"`
BoundClaims map[string]interface{} `json:"bound_claims"`
ClaimMappings map[string]string `json:"claim_mappings"`
BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"`
UserClaim string `json:"user_claim"`
GroupsClaim string `json:"groups_claim"`
OIDCScopes []string `json:"oidc_scopes"`
AllowedRedirectURIs []string `json:"allowed_redirect_uris"`
}
// role takes a storage backend and the name and returns the role's storage
@ -182,17 +226,20 @@ func (b *jwtAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request,
// Create a map of data to be returned
resp := &logical.Response{
Data: map[string]interface{}{
"policies": role.Policies,
"num_uses": role.NumUses,
"period": int64(role.Period.Seconds()),
"ttl": int64(role.TTL.Seconds()),
"max_ttl": int64(role.MaxTTL.Seconds()),
"bound_audiences": role.BoundAudiences,
"bound_subject": role.BoundSubject,
"bound_cidrs": role.BoundCIDRs,
"user_claim": role.UserClaim,
"groups_claim": role.GroupsClaim,
"groups_claim_delimiter_pattern": role.GroupsClaimDelimiterPattern,
"role_type": role.RoleType,
"policies": role.Policies,
"num_uses": role.NumUses,
"period": int64(role.Period.Seconds()),
"ttl": int64(role.TTL.Seconds()),
"max_ttl": int64(role.MaxTTL.Seconds()),
"bound_audiences": role.BoundAudiences,
"bound_subject": role.BoundSubject,
"bound_cidrs": role.BoundCIDRs,
"bound_claims": role.BoundClaims,
"claim_mappings": role.ClaimMappings,
"user_claim": role.UserClaim,
"groups_claim": role.GroupsClaim,
"allowed_redirect_uris": role.AllowedRedirectURIs,
},
}
@ -236,6 +283,15 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
role = new(jwtRole)
}
roleType := data.Get("role_type").(string)
if roleType == "" {
roleType = "jwt"
}
if roleType != "jwt" && roleType != "oidc" {
return logical.ErrorResponse("invalid 'role_type': %s", roleType), nil
}
role.RoleType = roleType
if policiesRaw, ok := data.GetOk("policies"); ok {
role.Policies = policyutil.ParsePolicies(policiesRaw)
}
@ -287,6 +343,29 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
role.BoundCIDRs = parsedCIDRs
}
if boundClaimsRaw, ok := data.GetOk("bound_claims"); ok {
role.BoundClaims = boundClaimsRaw.(map[string]interface{})
}
if claimMappingsRaw, ok := data.GetOk("claim_mappings"); ok {
claimMappings := claimMappingsRaw.(map[string]string)
// sanity check mappings for duplicates and collision with reserved names
targets := make(map[string]bool)
for _, metadataKey := range claimMappings {
if strutil.StrListContains(reservedMetadata, metadataKey) {
return logical.ErrorResponse("metadata key '%s' is reserved and may not be a mapping destination", metadataKey), nil
}
if targets[metadataKey] {
return logical.ErrorResponse("multiple keys are mapped to metadata key '%s'", metadataKey), nil
}
targets[metadataKey] = true
}
role.ClaimMappings = claimMappings
}
if userClaim, ok := data.GetOk("user_claim"); ok {
role.UserClaim = userClaim.(string)
}
@ -298,23 +377,26 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
role.GroupsClaim = groupsClaim.(string)
}
if groupsClaimDelimiterPattern, ok := data.GetOk("groups_claim_delimiter_pattern"); ok {
role.GroupsClaimDelimiterPattern = groupsClaimDelimiterPattern.(string)
if oidcScopes, ok := data.GetOk("oidc_scopes"); ok {
role.OIDCScopes = oidcScopes.([]string)
}
// Validate claim/delims
if role.GroupsClaim != "" {
if _, err := parseClaimWithDelimiters(role.GroupsClaim, role.GroupsClaimDelimiterPattern); err != nil {
return logical.ErrorResponse(errwrap.Wrapf("error validating delimiters for groups claim: {{err}}", err).Error()), nil
allowedRedirectURIs := data.Get("allowed_redirect_uris").([]string)
if roleType == "oidc" && len(allowedRedirectURIs) == 0 {
return logical.ErrorResponse("'allowed_redirect_uris' must be set"), nil
}
role.AllowedRedirectURIs = allowedRedirectURIs
// OIDC verifcation will enforce that the audience match the configured client_id.
// For other methods, require at least one bound constraint.
if roleType != "oidc" {
if len(role.BoundAudiences) == 0 &&
len(role.BoundCIDRs) == 0 &&
role.BoundSubject == "" {
return logical.ErrorResponse("must have at least one bound constraint when creating/updating a role"), nil
}
}
if len(role.BoundAudiences) == 0 &&
len(role.BoundCIDRs) == 0 &&
role.BoundSubject == "" {
return logical.ErrorResponse("must have at least one bound constraint when creating/updating a role"), nil
}
// Check that the TTL value provided is less than the MaxTTL.
// Sanitizing the TTL and MaxTTL is not required now and can be performed
// at credential issue time.
@ -340,32 +422,6 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
return resp, nil
}
// parseClaimWithDelimiters parses a given claim string and ensures that we can
// separate it out into a "map path"
func parseClaimWithDelimiters(claim, delimiters string) ([]string, error) {
if delimiters == "" {
return []string{claim}, nil
}
var ret []string
for _, runeVal := range delimiters {
idx := strings.IndexRune(claim, runeVal)
switch idx {
case -1:
return nil, fmt.Errorf("could not find instance of %q delimiter in claim", string(runeVal))
case 0:
return nil, fmt.Errorf("instance of %q delimiter in claim is at beginning of claim string", string(runeVal))
case len(claim) - 1:
return nil, fmt.Errorf("instance of %q delimiter in claim is at end of claim string", string(runeVal))
default:
ret = append(ret, claim[:idx])
claim = claim[idx+1:]
}
}
ret = append(ret, claim)
return ret, nil
}
// roleStorageEntry stores all the options that are set on an role
var roleHelp = map[string][2]string{
"role-list": {

View file

@ -0,0 +1,37 @@
// A throwaway file for super simple testing via a UI
package jwtauth
import (
"context"
"io/ioutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathUI(b *jwtAuthBackend) *framework.Path {
return &framework.Path{
Pattern: `ui$`,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathUI,
},
}
}
func (b *jwtAuthBackend) pathUI(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
data, err := ioutil.ReadFile("test_ui.html")
if err != nil {
panic(err)
}
resp := &logical.Response{
Data: map[string]interface{}{
logical.HTTPStatusCode: 200,
logical.HTTPRawBody: string(data),
logical.HTTPContentType: "text/html",
},
}
return resp, nil
}

View file

@ -0,0 +1,37 @@
<!-- A throwaway file for super simple testing via a UI -->
<html>
<body>
Role:<br>
<input id="role" type="text" value="test"/><br>
<button id="login">Login</button>
<script>
document.getElementById("login").addEventListener("click", doLogin);
function doLogin() {
role = document.getElementById("role").value;
fetch(`${window.location.origin}/v1/auth/jwt/oidc/auth_url`, {
method: "POST",
body: JSON.stringify({
role: role,
redirect_uri: `${window.location.origin}/v1/auth/jwt/oidc/callback`
})
}).then(function(response) {
return response.json();
}).then(function(myJSON) {
oidcAuth(myJSON.data.auth_url);
});
}
function oidcAuth(url) {
console.log(url);
url = url.replace("vaultserver", window.location.origin);
console.log(url);
location.replace(url);
}
</script>
</body>
</html>

74
vendor/github.com/mitchellh/pointerstructure/README.md generated vendored Normal file
View file

@ -0,0 +1,74 @@
# pointerstructure [![GoDoc](https://godoc.org/github.com/mitchellh/pointerstructure?status.svg)](https://godoc.org/github.com/mitchellh/pointerstructure)
pointerstructure is a Go library for identifying a specific value within
any Go structure using a string syntax.
pointerstructure is based on
[JSON Pointer (RFC 6901)](https://tools.ietf.org/html/rfc6901), but
reimplemented for Go.
The goal of pointerstructure is to provide a single, well-known format
for addressing a specific value. This can be useful for user provided
input on structures, diffs of structures, etc.
## Features
* Get the value for an address
* Set the value for an address within an existing structure
* Delete the value at an address
* Sorting a list of addresses
## Installation
Standard `go get`:
```
$ go get github.com/mitchellh/pointerstructure
```
## Usage & Example
For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/pointerstructure).
A quick code example is shown below:
```go
complex := map[string]interface{}{
"alice": 42,
"bob": []interface{}{
map[string]interface{}{
"name": "Bob",
},
},
}
value, err := pointerstructure.Get(complex, "/bob/0/name")
if err != nil {
panic(err)
}
fmt.Printf("%s", value)
// Output:
// Bob
```
Continuing the example above, you can also set values:
```go
value, err = pointerstructure.Set(complex, "/bob/0/name", "Alice")
if err != nil {
panic(err)
}
value, err = pointerstructure.Get(complex, "/bob/0/name")
if err != nil {
panic(err)
}
fmt.Printf("%s", value)
// Output:
// Alice
```

112
vendor/github.com/mitchellh/pointerstructure/delete.go generated vendored Normal file
View file

@ -0,0 +1,112 @@
package pointerstructure
import (
"fmt"
"reflect"
)
// Delete deletes the value specified by the pointer p in structure s.
//
// When deleting a slice index, all other elements will be shifted to
// the left. This is specified in RFC6902 (JSON Patch) and not RFC6901 since
// RFC6901 doesn't specify operations on pointers. If you don't want to
// shift elements, you should use Set to set the slice index to the zero value.
//
// The structures s must have non-zero values set up to this pointer.
// For example, if deleting "/bob/0/name", then "/bob/0" must be set already.
//
// The returned value is potentially a new value if this pointer represents
// the root document. Otherwise, the returned value will always be s.
func (p *Pointer) Delete(s interface{}) (interface{}, error) {
// if we represent the root doc, we've deleted everything
if len(p.Parts) == 0 {
return nil, nil
}
// Save the original since this is going to be our return value
originalS := s
// Get the parent value
var err error
s, err = p.Parent().Get(s)
if err != nil {
return nil, err
}
// Map for lookup of getter to call for type
funcMap := map[reflect.Kind]deleteFunc{
reflect.Array: p.deleteSlice,
reflect.Map: p.deleteMap,
reflect.Slice: p.deleteSlice,
}
val := reflect.ValueOf(s)
for val.Kind() == reflect.Interface {
val = val.Elem()
}
for val.Kind() == reflect.Ptr {
val = reflect.Indirect(val)
}
f, ok := funcMap[val.Kind()]
if !ok {
return nil, fmt.Errorf("delete %s: invalid value kind: %s", p, val.Kind())
}
result, err := f(originalS, val)
if err != nil {
return nil, fmt.Errorf("delete %s: %s", p, err)
}
return result, nil
}
type deleteFunc func(interface{}, reflect.Value) (interface{}, error)
func (p *Pointer) deleteMap(root interface{}, m reflect.Value) (interface{}, error) {
part := p.Parts[len(p.Parts)-1]
key, err := coerce(reflect.ValueOf(part), m.Type().Key())
if err != nil {
return root, err
}
// Delete the key
var elem reflect.Value
m.SetMapIndex(key, elem)
return root, nil
}
func (p *Pointer) deleteSlice(root interface{}, s reflect.Value) (interface{}, error) {
// Coerce the key to an int
part := p.Parts[len(p.Parts)-1]
idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42))
if err != nil {
return root, err
}
idx := int(idxVal.Int())
// Verify we're within bounds
if idx < 0 || idx >= s.Len() {
return root, fmt.Errorf(
"index %d is out of range (length = %d)", idx, s.Len())
}
// Mimicing the following with reflection to do this:
//
// copy(a[i:], a[i+1:])
// a[len(a)-1] = nil // or the zero value of T
// a = a[:len(a)-1]
// copy(a[i:], a[i+1:])
reflect.Copy(s.Slice(idx, s.Len()), s.Slice(idx+1, s.Len()))
// a[len(a)-1] = nil // or the zero value of T
s.Index(s.Len() - 1).Set(reflect.Zero(s.Type().Elem()))
// a = a[:len(a)-1]
s = s.Slice(0, s.Len()-1)
// set the slice back on the parent
return p.Parent().Set(root, s.Interface())
}

91
vendor/github.com/mitchellh/pointerstructure/get.go generated vendored Normal file
View file

@ -0,0 +1,91 @@
package pointerstructure
import (
"fmt"
"reflect"
)
// Get reads the value out of the total value v.
func (p *Pointer) Get(v interface{}) (interface{}, error) {
// fast-path the empty address case to avoid reflect.ValueOf below
if len(p.Parts) == 0 {
return v, nil
}
// Map for lookup of getter to call for type
funcMap := map[reflect.Kind]func(string, reflect.Value) (reflect.Value, error){
reflect.Array: p.getSlice,
reflect.Map: p.getMap,
reflect.Slice: p.getSlice,
}
currentVal := reflect.ValueOf(v)
for i, part := range p.Parts {
for currentVal.Kind() == reflect.Interface {
currentVal = currentVal.Elem()
}
for currentVal.Kind() == reflect.Ptr {
currentVal = reflect.Indirect(currentVal)
}
f, ok := funcMap[currentVal.Kind()]
if !ok {
return nil, fmt.Errorf(
"%s: at part %d, invalid value kind: %s", p, i, currentVal.Kind())
}
var err error
currentVal, err = f(part, currentVal)
if err != nil {
return nil, fmt.Errorf("%s at part %d: %s", p, i, err)
}
}
return currentVal.Interface(), nil
}
func (p *Pointer) getMap(part string, m reflect.Value) (reflect.Value, error) {
var zeroValue reflect.Value
// Coerce the string part to the correct key type
key, err := coerce(reflect.ValueOf(part), m.Type().Key())
if err != nil {
return zeroValue, err
}
// Verify that the key exists
found := false
for _, k := range m.MapKeys() {
if k.Interface() == key.Interface() {
found = true
break
}
}
if !found {
return zeroValue, fmt.Errorf("couldn't find key %#v", key.Interface())
}
// Get the key
return m.MapIndex(key), nil
}
func (p *Pointer) getSlice(part string, v reflect.Value) (reflect.Value, error) {
var zeroValue reflect.Value
// Coerce the key to an int
idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42))
if err != nil {
return zeroValue, err
}
idx := int(idxVal.Int())
// Verify we're within bounds
if idx < 0 || idx >= v.Len() {
return zeroValue, fmt.Errorf(
"index %d is out of range (length = %d)", idx, v.Len())
}
// Get the key
return v.Index(idx), nil
}

57
vendor/github.com/mitchellh/pointerstructure/parse.go generated vendored Normal file
View file

@ -0,0 +1,57 @@
package pointerstructure
import (
"fmt"
"strings"
)
// Parse parses a pointer from the input string. The input string
// is expected to follow the format specified by RFC 6901: '/'-separated
// parts. Each part can contain escape codes to contain '/' or '~'.
func Parse(input string) (*Pointer, error) {
// Special case the empty case
if input == "" {
return &Pointer{}, nil
}
// We expect the first character to be "/"
if input[0] != '/' {
return nil, fmt.Errorf(
"parse Go pointer %q: first char must be '/'", input)
}
// Trim out the first slash so we don't have to +1 every index
input = input[1:]
// Parse out all the parts
var parts []string
lastSlash := -1
for i, r := range input {
if r == '/' {
parts = append(parts, input[lastSlash+1:i])
lastSlash = i
}
}
// Add last part
parts = append(parts, input[lastSlash+1:])
// Process each part for string replacement
for i, p := range parts {
// Replace ~1 followed by ~0 as specified by the RFC
parts[i] = strings.Replace(
strings.Replace(p, "~1", "/", -1), "~0", "~", -1)
}
return &Pointer{Parts: parts}, nil
}
// MustParse is like Parse but panics if the input cannot be parsed.
func MustParse(input string) *Pointer {
p, err := Parse(input)
if err != nil {
panic(err)
}
return p
}

123
vendor/github.com/mitchellh/pointerstructure/pointer.go generated vendored Normal file
View file

@ -0,0 +1,123 @@
// Package pointerstructure provides functions for identifying a specific
// value within any Go structure using a string syntax.
//
// The syntax used is based on JSON Pointer (RFC 6901).
package pointerstructure
import (
"fmt"
"reflect"
"strings"
"github.com/mitchellh/mapstructure"
)
// Pointer represents a pointer to a specific value. You can construct
// a pointer manually or use Parse.
type Pointer struct {
// Parts are the pointer parts. No escape codes are processed here.
// The values are expected to be exact. If you have escape codes, use
// the Parse functions.
Parts []string
}
// Get reads the value at the given pointer.
//
// This is a shorthand for calling Parse on the pointer and then calling Get
// on that result. An error will be returned if the value cannot be found or
// there is an error with the format of pointer.
func Get(value interface{}, pointer string) (interface{}, error) {
p, err := Parse(pointer)
if err != nil {
return nil, err
}
return p.Get(value)
}
// Set sets the value at the given pointer.
//
// This is a shorthand for calling Parse on the pointer and then calling Set
// on that result. An error will be returned if the value cannot be found or
// there is an error with the format of pointer.
//
// Set returns the complete document, which might change if the pointer value
// points to the root ("").
func Set(doc interface{}, pointer string, value interface{}) (interface{}, error) {
p, err := Parse(pointer)
if err != nil {
return nil, err
}
return p.Set(doc, value)
}
// String returns the string value that can be sent back to Parse to get
// the same Pointer result.
func (p *Pointer) String() string {
if len(p.Parts) == 0 {
return ""
}
// Copy the parts so we can convert back the escapes
result := make([]string, len(p.Parts))
copy(result, p.Parts)
for i, p := range p.Parts {
result[i] = strings.Replace(
strings.Replace(p, "~", "~0", -1), "/", "~1", -1)
}
return "/" + strings.Join(result, "/")
}
// Parent returns a pointer to the parent element of this pointer.
//
// If Pointer represents the root (empty parts), a pointer representing
// the root is returned. Therefore, to check for the root, IsRoot() should be
// called.
func (p *Pointer) Parent() *Pointer {
// If this is root, then we just return a new root pointer. We allocate
// a new one though so this can still be modified.
if p.IsRoot() {
return &Pointer{}
}
parts := make([]string, len(p.Parts)-1)
copy(parts, p.Parts[:len(p.Parts)-1])
return &Pointer{
Parts: parts,
}
}
// IsRoot returns true if this pointer represents the root document.
func (p *Pointer) IsRoot() bool {
return len(p.Parts) == 0
}
// coerce is a helper to coerce a value to a specific type if it must
// and if its possible. If it isn't possible, an error is returned.
func coerce(value reflect.Value, to reflect.Type) (reflect.Value, error) {
// If the value is already assignable to the type, then let it go
if value.Type().AssignableTo(to) {
return value, nil
}
// If a direct conversion is possible, do that
if value.Type().ConvertibleTo(to) {
return value.Convert(to), nil
}
// Create a new value to hold our result
result := reflect.New(to)
// Decode
if err := mapstructure.WeakDecode(value.Interface(), result.Interface()); err != nil {
return result, fmt.Errorf(
"couldn't convert value %#v to type %s",
value.Interface(), to.String())
}
// We need to indirect the value since reflect.New always creates a pointer
return reflect.Indirect(result), nil
}

122
vendor/github.com/mitchellh/pointerstructure/set.go generated vendored Normal file
View file

@ -0,0 +1,122 @@
package pointerstructure
import (
"fmt"
"reflect"
)
// Set writes a value v to the pointer p in structure s.
//
// The structures s must have non-zero values set up to this pointer.
// For example, if setting "/bob/0/name", then "/bob/0" must be set already.
//
// The returned value is potentially a new value if this pointer represents
// the root document. Otherwise, the returned value will always be s.
func (p *Pointer) Set(s, v interface{}) (interface{}, error) {
// if we represent the root doc, return that
if len(p.Parts) == 0 {
return v, nil
}
// Save the original since this is going to be our return value
originalS := s
// Get the parent value
var err error
s, err = p.Parent().Get(s)
if err != nil {
return nil, err
}
// Map for lookup of getter to call for type
funcMap := map[reflect.Kind]setFunc{
reflect.Array: p.setSlice,
reflect.Map: p.setMap,
reflect.Slice: p.setSlice,
}
val := reflect.ValueOf(s)
for val.Kind() == reflect.Interface {
val = val.Elem()
}
for val.Kind() == reflect.Ptr {
val = reflect.Indirect(val)
}
f, ok := funcMap[val.Kind()]
if !ok {
return nil, fmt.Errorf("set %s: invalid value kind: %s", p, val.Kind())
}
result, err := f(originalS, val, reflect.ValueOf(v))
if err != nil {
return nil, fmt.Errorf("set %s: %s", p, err)
}
return result, nil
}
type setFunc func(interface{}, reflect.Value, reflect.Value) (interface{}, error)
func (p *Pointer) setMap(root interface{}, m, value reflect.Value) (interface{}, error) {
part := p.Parts[len(p.Parts)-1]
key, err := coerce(reflect.ValueOf(part), m.Type().Key())
if err != nil {
return root, err
}
elem, err := coerce(value, m.Type().Elem())
if err != nil {
return root, err
}
// Set the key
m.SetMapIndex(key, elem)
return root, nil
}
func (p *Pointer) setSlice(root interface{}, s, value reflect.Value) (interface{}, error) {
// Coerce the value, we'll need that no matter what
value, err := coerce(value, s.Type().Elem())
if err != nil {
return root, err
}
// If the part is the special "-", that means to append it (RFC6901 4.)
part := p.Parts[len(p.Parts)-1]
if part == "-" {
return p.setSliceAppend(root, s, value)
}
// Coerce the key to an int
idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42))
if err != nil {
return root, err
}
idx := int(idxVal.Int())
// Verify we're within bounds
if idx < 0 || idx >= s.Len() {
return root, fmt.Errorf(
"index %d is out of range (length = %d)", idx, s.Len())
}
// Set the key
s.Index(idx).Set(value)
return root, nil
}
func (p *Pointer) setSliceAppend(root interface{}, s, value reflect.Value) (interface{}, error) {
// Coerce the value, we'll need that no matter what. This should
// be a no-op since we expect it to be done already, but there is
// a fast-path check for that in coerce so do it anyways.
value, err := coerce(value, s.Type().Elem())
if err != nil {
return root, err
}
// We can assume "s" is the parent of pointer value. We need to actually
// write s back because Append can return a new slice.
return p.Parent().Set(root, reflect.Append(s, value).Interface())
}

42
vendor/github.com/mitchellh/pointerstructure/sort.go generated vendored Normal file
View file

@ -0,0 +1,42 @@
package pointerstructure
import (
"sort"
)
// Sort does an in-place sort of the pointers so that they are in order
// of least specific to most specific alphabetized. For example:
// "/foo", "/foo/0", "/qux"
//
// This ordering is ideal for applying the changes in a way that ensures
// that parents are set first.
func Sort(p []*Pointer) { sort.Sort(PointerSlice(p)) }
// PointerSlice is a slice of pointers that adheres to sort.Interface
type PointerSlice []*Pointer
func (p PointerSlice) Len() int { return len(p) }
func (p PointerSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p PointerSlice) Less(i, j int) bool {
// Equal number of parts, do a string compare per part
for idx, ival := range p[i].Parts {
// If we're passed the length of p[j] parts, then we're done
if idx >= len(p[j].Parts) {
break
}
// Compare the values if they're not equal
jval := p[j].Parts[idx]
if ival != jval {
return ival < jval
}
}
// Equal prefix, take the shorter
if len(p[i].Parts) != len(p[j].Parts) {
return len(p[i].Parts) < len(p[j].Parts)
}
// Equal, it doesn't matter
return false
}

14
vendor/vendor.json vendored
View file

@ -1409,10 +1409,12 @@
"revisionTime": "2018-12-10T20:01:33Z"
},
{
"checksumSHA1": "tt3FtyjXgdBI9Mb43UL4LtOZmAk=",
"checksumSHA1": "86jzaGc3dRpZ5BKQPFP7ecasQfg=",
"path": "github.com/hashicorp/vault-plugin-auth-jwt",
"revision": "f428c77917331c1b87dae2dd37016bd1dd4c55da",
"revisionTime": "2018-10-31T19:59:42Z"
"revision": "bf17a88bb5c43eb2cbdc08011cd76ecec028521c",
"revisionTime": "2019-02-07T06:35:46Z",
"version": "=oidc-cli",
"versionExact": "oidc-cli"
},
{
"checksumSHA1": "Ldg2jQeyPrpAupyQq4lRVN+jfFY=",
@ -1788,6 +1790,12 @@
"revision": "3536a929edddb9a5b34bd6861dc4a9647cb459fe",
"revisionTime": "2018-10-05T04:51:35Z"
},
{
"checksumSHA1": "31atAEqGt+z8hZgyVZZokEeM6dM=",
"path": "github.com/mitchellh/pointerstructure",
"revision": "f2329fcfa9e280bdb5a3f2544aec815a508ad72f",
"revisionTime": "2017-02-05T20:42:03Z"
},
{
"checksumSHA1": "nxuST3bjBv5uDVPzrX9wdruOwv0=",
"path": "github.com/mitchellh/reflectwalk",