Merge pull request #1670 from smallstep/herman/remove-rusty-cli
Remove `rusty-jwt-cli`pull/1683/head
commit
7e6356ece2
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,394 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
certificatesdb "github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDB_GetDpopToken(t *testing.T) {
|
||||
type test struct {
|
||||
db *DB
|
||||
orderID string
|
||||
expected map[string]any
|
||||
expectedErr error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/acme-not-found": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: &acme.Error{
|
||||
Type: "urn:ietf:params:acme:error:malformed",
|
||||
Status: 400,
|
||||
Detail: "The request message was malformed",
|
||||
Err: errors.New(`dpop token "orderID" not found`),
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
token := dbDpopToken{
|
||||
ID: "orderID",
|
||||
Content: []byte("{}"),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
b, err := json.Marshal(token)
|
||||
require.NoError(t, err)
|
||||
err = db.Set(wireDpopTokenTable, []byte("orderID"), b[1:]) // start at index 1; corrupt JSON data
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: errors.New(`failed unmarshaling dpop token "orderID" into dbDpopToken: invalid character ':' after top-level value`),
|
||||
}
|
||||
},
|
||||
"fail/db.Get": func(t *testing.T) test {
|
||||
db := &certificatesdb.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equal(t, wireDpopTokenTable, bucket)
|
||||
assert.Equal(t, []byte("orderID"), key)
|
||||
return nil, errors.New("fail")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: errors.New(`failed loading dpop token "orderID": fail`),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
token := dbDpopToken{
|
||||
ID: "orderID",
|
||||
Content: []byte(`{"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com"}`),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
b, err := json.Marshal(token)
|
||||
require.NoError(t, err)
|
||||
err = db.Set(wireDpopTokenTable, []byte("orderID"), b)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expected: map[string]any{
|
||||
"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := tc.db.GetDpopToken(context.Background(), tc.orderID)
|
||||
if tc.expectedErr != nil {
|
||||
assert.EqualError(t, err, tc.expectedErr.Error())
|
||||
ae := &acme.Error{}
|
||||
if errors.As(err, &ae) {
|
||||
ee := &acme.Error{}
|
||||
require.True(t, errors.As(tc.expectedErr, &ee))
|
||||
assert.Equal(t, ee.Detail, ae.Detail)
|
||||
assert.Equal(t, ee.Type, ae.Type)
|
||||
assert.Equal(t, ee.Status, ae.Status)
|
||||
}
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_CreateDpopToken(t *testing.T) {
|
||||
type test struct {
|
||||
db *DB
|
||||
orderID string
|
||||
dpop map[string]any
|
||||
expectedErr error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Save": func(t *testing.T) test {
|
||||
db := &certificatesdb.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
assert.Equal(t, wireDpopTokenTable, bucket)
|
||||
assert.Equal(t, []byte("orderID"), key)
|
||||
return nil, false, errors.New("fail")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
dpop: map[string]any{
|
||||
"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com",
|
||||
},
|
||||
expectedErr: errors.New("failed saving dpop token: error saving acme dpop: fail"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
dpop: map[string]any{
|
||||
"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/nil": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
dpop: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := tc.db.CreateDpopToken(context.Background(), tc.orderID, tc.dpop)
|
||||
if tc.expectedErr != nil {
|
||||
assert.EqualError(t, err, tc.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
dpop, err := tc.db.getDBDpopToken(context.Background(), tc.orderID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.orderID, dpop.ID)
|
||||
var m map[string]any
|
||||
err = json.Unmarshal(dpop.Content, &m)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.dpop, m)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetOidcToken(t *testing.T) {
|
||||
type test struct {
|
||||
db *DB
|
||||
orderID string
|
||||
expected map[string]any
|
||||
expectedErr error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/acme-not-found": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: &acme.Error{
|
||||
Type: "urn:ietf:params:acme:error:malformed",
|
||||
Status: 400,
|
||||
Detail: "The request message was malformed",
|
||||
Err: errors.New(`oidc token "orderID" not found`),
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
token := dbOidcToken{
|
||||
ID: "orderID",
|
||||
Content: []byte("{}"),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
b, err := json.Marshal(token)
|
||||
require.NoError(t, err)
|
||||
err = db.Set(wireOidcTokenTable, []byte("orderID"), b[1:]) // start at index 1; corrupt JSON data
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: errors.New(`failed unmarshaling oidc token "orderID" into dbOidcToken: invalid character ':' after top-level value`),
|
||||
}
|
||||
},
|
||||
"fail/db.Get": func(t *testing.T) test {
|
||||
db := &certificatesdb.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equal(t, wireOidcTokenTable, bucket)
|
||||
assert.Equal(t, []byte("orderID"), key)
|
||||
return nil, errors.New("fail")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expectedErr: errors.New(`failed loading oidc token "orderID": fail`),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
token := dbOidcToken{
|
||||
ID: "orderID",
|
||||
Content: []byte(`{"name": "Alice Smith", "preferred_username": "@alice.smith"}`),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
b, err := json.Marshal(token)
|
||||
require.NoError(t, err)
|
||||
err = db.Set(wireOidcTokenTable, []byte("orderID"), b)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
expected: map[string]any{
|
||||
"name": "Alice Smith",
|
||||
"preferred_username": "@alice.smith",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := tc.db.GetOidcToken(context.Background(), tc.orderID)
|
||||
if tc.expectedErr != nil {
|
||||
assert.EqualError(t, err, tc.expectedErr.Error())
|
||||
ae := &acme.Error{}
|
||||
if errors.As(err, &ae) {
|
||||
ee := &acme.Error{}
|
||||
require.True(t, errors.As(tc.expectedErr, &ee))
|
||||
assert.Equal(t, ee.Detail, ae.Detail)
|
||||
assert.Equal(t, ee.Type, ae.Type)
|
||||
assert.Equal(t, ee.Status, ae.Status)
|
||||
}
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_CreateOidcToken(t *testing.T) {
|
||||
type test struct {
|
||||
db *DB
|
||||
orderID string
|
||||
oidc map[string]any
|
||||
expectedErr error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Save": func(t *testing.T) test {
|
||||
db := &certificatesdb.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
assert.Equal(t, wireOidcTokenTable, bucket)
|
||||
assert.Equal(t, []byte("orderID"), key)
|
||||
return nil, false, errors.New("fail")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
oidc: map[string]any{
|
||||
"name": "Alice Smith",
|
||||
"preferred_username": "@alice.smith",
|
||||
},
|
||||
expectedErr: errors.New("failed saving oidc token: error saving acme oidc: fail"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
oidc: map[string]any{
|
||||
"name": "Alice Smith",
|
||||
"preferred_username": "@alice.smith",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/nil": func(t *testing.T) test {
|
||||
dir := t.TempDir()
|
||||
db, err := nosql.New("badgerv2", dir)
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
db: &DB{
|
||||
db: db,
|
||||
},
|
||||
orderID: "orderID",
|
||||
oidc: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := tc.db.CreateOidcToken(context.Background(), tc.orderID, tc.oidc)
|
||||
if tc.expectedErr != nil {
|
||||
assert.EqualError(t, err, tc.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
oidc, err := tc.db.getDBOidcToken(context.Background(), tc.orderID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.orderID, oidc.ID)
|
||||
var m map[string]any
|
||||
err = json.Unmarshal(oidc.Content, &m)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.oidc, m)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseID(t *testing.T) {
|
||||
ok := `{"name": "Alice Smith", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wantWireID ID
|
||||
expectedErr error
|
||||
}{
|
||||
{name: "ok", data: []byte(ok), wantWireID: ID{Name: "Alice Smith", Domain: "wire.com", ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", Handle: "wireapp://%40alice_wire@wire.com"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotWireID, err := ParseID(tt.data)
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantWireID, gotWireID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseClientID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
clientID string
|
||||
want ClientID
|
||||
expectedErr error
|
||||
}{
|
||||
{name: "ok", clientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", want: ClientID{Scheme: "wireapp", Username: "CzbfFjDOQrenCbDxVmgnFw", DeviceID: "594930e9d50bb175", Domain: "wire.com"}},
|
||||
{name: "fail/uri", clientID: "bla", expectedErr: errors.New(`invalid Wire client ID URI "bla": error parsing bla: scheme is missing`)},
|
||||
{name: "fail/scheme", clientID: "not-wireapp://bla.com", expectedErr: errors.New(`invalid Wire client ID scheme "not-wireapp"; expected "wireapp"`)},
|
||||
{name: "fail/username", clientID: "wireapp://user@wire.com", expectedErr: errors.New(`invalid Wire client ID username "user"`)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseClientID(tt.clientID)
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type DPOPOptions struct {
|
||||
// ValidationExecPath is the name of the executable to call for DPOP
|
||||
// validation.
|
||||
ValidationExecPath string `json:"validation-exec-path,omitempty"`
|
||||
// Backend signing key for DPoP access token
|
||||
SigningKey string `json:"key"`
|
||||
// URI template acme client must call to fetch the DPoP challenge proof (an access token from wire-server)
|
||||
DpopTarget string `json:"dpop-target"`
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) GetValidationExecPath() string {
|
||||
if o == nil {
|
||||
return "rusty-jwt-cli"
|
||||
}
|
||||
return o.ValidationExecPath
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) GetSigningKey() string {
|
||||
if o == nil {
|
||||
return ""
|
||||
}
|
||||
return o.SigningKey
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) GetDPOPTarget() string {
|
||||
if o == nil {
|
||||
return ""
|
||||
}
|
||||
return o.DpopTarget
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) GetTarget(deviceID string) (string, error) {
|
||||
if o == nil {
|
||||
return "", errors.New("misconfigured target template configuration")
|
||||
}
|
||||
targetTemplate := o.GetDPOPTarget()
|
||||
tmpl, err := template.New("DeviceId").Parse(targetTemplate)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed parsing dpop template: %w", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err = tmpl.Execute(buf, struct{ DeviceId string }{DeviceId: deviceID}); err != nil { //nolint:revive,stylecheck // TODO(hs): this requires changes in configuration
|
||||
return "", fmt.Errorf("failed executing dpop template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
)
|
||||
|
||||
type ProviderJSON struct {
|
||||
IssuerURL string `json:"issuer,omitempty"`
|
||||
AuthURL string `json:"authorization_endpoint,omitempty"`
|
||||
TokenURL string `json:"token_endpoint,omitempty"`
|
||||
JWKSURL string `json:"jwks_uri,omitempty"`
|
||||
UserInfoURL string `json:"userinfo_endpoint,omitempty"`
|
||||
Algorithms []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigJSON struct {
|
||||
ClientID string `json:"client-id,omitempty"`
|
||||
SupportedSigningAlgs []string `json:"support-signing-algs,omitempty"`
|
||||
SkipClientIDCheck bool `json:"-"`
|
||||
SkipExpiryCheck bool `json:"-"`
|
||||
SkipIssuerCheck bool `json:"-"`
|
||||
Now func() time.Time `json:"-"`
|
||||
InsecureSkipSignatureCheck bool `json:"-"`
|
||||
}
|
||||
|
||||
type OIDCOptions struct {
|
||||
Provider ProviderJSON `json:"provider,omitempty"`
|
||||
Config ConfigJSON `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) GetProvider(ctx context.Context) *oidc.Provider {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return toProviderConfig(o.Provider).NewProvider(ctx)
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) GetConfig() *oidc.Config {
|
||||
if o == nil {
|
||||
return &oidc.Config{}
|
||||
}
|
||||
config := oidc.Config(o.Config)
|
||||
return &config
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) GetTarget(deviceID string) (string, error) {
|
||||
if o == nil {
|
||||
return "", errors.New("misconfigured target template configuration")
|
||||
}
|
||||
targetTemplate := o.Provider.IssuerURL
|
||||
tmpl, err := template.New("DeviceId").Parse(targetTemplate)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed parsing oidc template: %w", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err = tmpl.Execute(buf, struct{ DeviceId string }{DeviceId: deviceID}); err != nil { //nolint:revive,stylecheck // TODO(hs): this requires changes in configuration
|
||||
return "", fmt.Errorf("failed executing oidc template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func toProviderConfig(in ProviderJSON) *oidc.ProviderConfig {
|
||||
issuerURL, err := url.Parse(in.IssuerURL)
|
||||
if err != nil {
|
||||
panic(err) // config error, it's ok to panic here
|
||||
}
|
||||
// Removes query params from the URL because we use it as a way to notify client about the actual OAuth ClientId
|
||||
// for this provisioner.
|
||||
// This URL is going to look like: "https://idp:5556/dex?clientid=foo"
|
||||
// If we don't trim the query params here i.e. 'clientid' then the idToken verification is going to fail because
|
||||
// the 'iss' claim of the idToken will be "https://idp:5556/dex"
|
||||
issuerURL.RawQuery = ""
|
||||
issuerURL.Fragment = ""
|
||||
return &oidc.ProviderConfig{
|
||||
IssuerURL: issuerURL.String(),
|
||||
AuthURL: in.AuthURL,
|
||||
TokenURL: in.TokenURL,
|
||||
UserInfoURL: in.UserInfoURL,
|
||||
JWKSURL: in.JWKSURL,
|
||||
Algorithms: in.Algorithms,
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"go.step.sm/crypto/pemutil"
|
||||
)
|
||||
|
||||
type DPOPOptions struct {
|
||||
// Public part of the signing key for DPoP access token in PEM format
|
||||
SigningKey []byte `json:"key"`
|
||||
// URI template for the URI the ACME client must call to fetch the DPoP challenge proof (an access token from wire-server)
|
||||
Target string `json:"target"`
|
||||
|
||||
signingKey crypto.PublicKey
|
||||
target *template.Template
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) GetSigningKey() crypto.PublicKey {
|
||||
return o.signingKey
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) EvaluateTarget(deviceID string) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil {
|
||||
return "", fmt.Errorf("failed executing dpop template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (o *DPOPOptions) validateAndInitialize() (err error) {
|
||||
o.signingKey, err = pemutil.Parse(o.SigningKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing key: %w", err)
|
||||
}
|
||||
o.target, err = template.New("DeviceID").Parse(o.Target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing DPoP template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
IssuerURL string `json:"issuer,omitempty"`
|
||||
AuthURL string `json:"authorization_endpoint,omitempty"`
|
||||
TokenURL string `json:"token_endpoint,omitempty"`
|
||||
JWKSURL string `json:"jwks_uri,omitempty"`
|
||||
UserInfoURL string `json:"userinfo_endpoint,omitempty"`
|
||||
Algorithms []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ClientID string `json:"clientId,omitempty"`
|
||||
SignatureAlgorithms []string `json:"signatureAlgorithms,omitempty"`
|
||||
|
||||
// the properties below are only used for testing
|
||||
SkipClientIDCheck bool `json:"-"`
|
||||
SkipExpiryCheck bool `json:"-"`
|
||||
SkipIssuerCheck bool `json:"-"`
|
||||
InsecureSkipSignatureCheck bool `json:"-"`
|
||||
Now func() time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type OIDCOptions struct {
|
||||
Provider *Provider `json:"provider,omitempty"`
|
||||
Config *Config `json:"config,omitempty"`
|
||||
TransformTemplate string `json:"transform,omitempty"`
|
||||
|
||||
oidcProviderConfig *oidc.ProviderConfig
|
||||
target *template.Template
|
||||
transform *template.Template
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) GetProvider(ctx context.Context) *oidc.Provider {
|
||||
if o == nil || o.Provider == nil || o.oidcProviderConfig == nil {
|
||||
return nil
|
||||
}
|
||||
return o.oidcProviderConfig.NewProvider(ctx)
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) GetConfig() *oidc.Config {
|
||||
if o == nil || o.Config == nil {
|
||||
return &oidc.Config{}
|
||||
}
|
||||
|
||||
return &oidc.Config{
|
||||
ClientID: o.Config.ClientID,
|
||||
SupportedSigningAlgs: o.Config.SignatureAlgorithms,
|
||||
SkipClientIDCheck: o.Config.SkipClientIDCheck,
|
||||
SkipExpiryCheck: o.Config.SkipExpiryCheck,
|
||||
SkipIssuerCheck: o.Config.SkipIssuerCheck,
|
||||
Now: o.Config.Now,
|
||||
InsecureSkipSignatureCheck: o.Config.InsecureSkipSignatureCheck,
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTemplate = `{"name": "{{ .name }}", "preferred_username": "{{ .preferred_username }}"}`
|
||||
|
||||
func (o *OIDCOptions) validateAndInitialize() (err error) {
|
||||
if o.Provider == nil {
|
||||
return errors.New("provider not set")
|
||||
}
|
||||
if o.Provider.IssuerURL == "" {
|
||||
return errors.New("issuer URL must not be empty")
|
||||
}
|
||||
|
||||
o.oidcProviderConfig, err = toOIDCProviderConfig(o.Provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creationg OIDC provider config: %w", err)
|
||||
}
|
||||
|
||||
o.target, err = template.New("DeviceID").Parse(o.Provider.IssuerURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing OIDC template: %w", err)
|
||||
}
|
||||
|
||||
o.transform, err = parseTransform(o.TransformTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing OIDC transformation template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTransform(transformTemplate string) (*template.Template, error) {
|
||||
if transformTemplate == "" {
|
||||
transformTemplate = defaultTemplate
|
||||
}
|
||||
|
||||
return template.New("transform").Funcs(x509util.GetFuncMap()).Parse(transformTemplate)
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) EvaluateTarget(deviceID string) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil {
|
||||
return "", fmt.Errorf("failed executing OIDC template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) Transform(v map[string]any) (map[string]any, error) {
|
||||
if o.transform == nil || v == nil {
|
||||
return v, nil
|
||||
}
|
||||
// TODO(hs): add support for extracting error message from template "fail" function?
|
||||
buf := new(bytes.Buffer)
|
||||
if err := o.transform.Execute(buf, v); err != nil {
|
||||
return nil, fmt.Errorf("failed executing OIDC transformation: %w", err)
|
||||
}
|
||||
var r map[string]any
|
||||
if err := json.Unmarshal(buf.Bytes(), &r); err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshaling transformed OIDC token: %w", err)
|
||||
}
|
||||
// add original claims if not yet in the transformed result
|
||||
for key, value := range v {
|
||||
if _, ok := r[key]; !ok {
|
||||
r[key] = value
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func toOIDCProviderConfig(in *Provider) (*oidc.ProviderConfig, error) {
|
||||
issuerURL, err := url.Parse(in.IssuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed parsing issuer URL: %w", err)
|
||||
}
|
||||
// Removes query params from the URL because we use it as a way to notify client about the actual OAuth ClientId
|
||||
// for this provisioner.
|
||||
// This URL is going to look like: "https://idp:5556/dex?clientid=foo"
|
||||
// If we don't trim the query params here i.e. 'clientid' then the idToken verification is going to fail because
|
||||
// the 'iss' claim of the idToken will be "https://idp:5556/dex"
|
||||
issuerURL.RawQuery = ""
|
||||
issuerURL.Fragment = ""
|
||||
return &oidc.ProviderConfig{
|
||||
IssuerURL: issuerURL.String(),
|
||||
AuthURL: in.AuthURL,
|
||||
TokenURL: in.TokenURL,
|
||||
UserInfoURL: in.UserInfoURL,
|
||||
JWKSURL: in.JWKSURL,
|
||||
Algorithms: in.Algorithms,
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOIDCOptions_Transform(t *testing.T) {
|
||||
defaultTransform, err := parseTransform(``)
|
||||
require.NoError(t, err)
|
||||
swapTransform, err := parseTransform(`{"name": "{{ .preferred_username }}", "preferred_username": "{{ .name }}"}`)
|
||||
require.NoError(t, err)
|
||||
funcTransform, err := parseTransform(`{"name": "{{ .name }}", "preferred_username": "{{ first .usernames }}"}`)
|
||||
require.NoError(t, err)
|
||||
type fields struct {
|
||||
transform *template.Template
|
||||
}
|
||||
type args struct {
|
||||
v map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want map[string]any
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "ok/no-transform",
|
||||
fields: fields{
|
||||
transform: nil,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok/empty-data",
|
||||
fields: fields{
|
||||
transform: nil,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{},
|
||||
},
|
||||
want: map[string]any{},
|
||||
},
|
||||
{
|
||||
name: "ok/default-transform",
|
||||
fields: fields{
|
||||
transform: defaultTransform,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok/swap-transform",
|
||||
fields: fields{
|
||||
transform: swapTransform,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "Preferred",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"name": "Preferred",
|
||||
"preferred_username": "Example",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok/transform-with-functions",
|
||||
fields: fields{
|
||||
transform: funcTransform,
|
||||
},
|
||||
args: args{
|
||||
v: map[string]any{
|
||||
"name": "Example",
|
||||
"usernames": []string{"name-1", "name-2", "name-3"},
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"name": "Example",
|
||||
"preferred_username": "name-1",
|
||||
"usernames": []string{"name-1", "name-2", "name-3"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &OIDCOptions{
|
||||
transform: tt.fields.transform,
|
||||
}
|
||||
got, err := o.Transform(tt.args.v)
|
||||
if tt.expectedErr != nil {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Options holds the Wire ACME extension options
|
||||
type Options struct {
|
||||
OIDC *OIDCOptions `json:"oidc,omitempty"`
|
||||
DPOP *DPOPOptions `json:"dpop,omitempty"`
|
||||
}
|
||||
|
||||
// GetOIDCOptions returns the OIDC options.
|
||||
func (o *Options) GetOIDCOptions() *OIDCOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.OIDC
|
||||
}
|
||||
|
||||
// GetDPOPOptions returns the DPoP options.
|
||||
func (o *Options) GetDPOPOptions() *DPOPOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.DPOP
|
||||
}
|
||||
|
||||
// Validate validates and initializes the Wire OIDC and DPoP options.
|
||||
//
|
||||
// TODO(hs): find a good way to perform this only once.
|
||||
func (o *Options) Validate() error {
|
||||
if oidc := o.GetOIDCOptions(); oidc != nil {
|
||||
if err := oidc.validateAndInitialize(); err != nil {
|
||||
return fmt.Errorf("failed initializing OIDC options: %w", err)
|
||||
}
|
||||
} else {
|
||||
return errors.New("no OIDC options available")
|
||||
}
|
||||
|
||||
if dpop := o.GetDPOPOptions(); dpop != nil {
|
||||
if err := dpop.validateAndInitialize(); err != nil {
|
||||
return fmt.Errorf("failed initializing DPoP options: %w", err)
|
||||
}
|
||||
} else {
|
||||
return errors.New("no DPoP options available")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptions_Validate(t *testing.T) {
|
||||
key := []byte(`-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
|
||||
-----END PUBLIC KEY-----`)
|
||||
|
||||
type fields struct {
|
||||
OIDC *OIDCOptions
|
||||
DPOP *DPOPOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{
|
||||
SigningKey: key,
|
||||
},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "fail/no-oidc-options",
|
||||
fields: fields{
|
||||
OIDC: nil,
|
||||
DPOP: &DPOPOptions{},
|
||||
},
|
||||
expectedErr: errors.New("no OIDC options available"),
|
||||
},
|
||||
{
|
||||
name: "fail/empty-issuer-url",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{},
|
||||
},
|
||||
expectedErr: errors.New("failed initializing OIDC options: issuer URL must not be empty"),
|
||||
},
|
||||
{
|
||||
name: "fail/invalid-issuer-url",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "\x00",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing OIDC options: failed creationg OIDC provider config: failed parsing issuer URL: parse "\x00": net/url: invalid control character in URL`),
|
||||
},
|
||||
{
|
||||
name: "fail/issuer-url-template",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://issuer.example.com/{{}",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC template: template: DeviceID:1: unexpected "}" in command`),
|
||||
},
|
||||
{
|
||||
name: "fail/invalid-transform-template",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
TransformTemplate: "{{}",
|
||||
},
|
||||
DPOP: &DPOPOptions{
|
||||
SigningKey: key,
|
||||
},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC transformation template: template: transform:1: unexpected "}" in command`),
|
||||
},
|
||||
{
|
||||
name: "fail/no-dpop-options",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: nil,
|
||||
},
|
||||
expectedErr: errors.New("no DPoP options available"),
|
||||
},
|
||||
{
|
||||
name: "fail/invalid-key",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{
|
||||
SigningKey: []byte{0x00},
|
||||
Target: "",
|
||||
},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing DPoP options: failed parsing key: error decoding PEM: not a valid PEM encoded block`),
|
||||
},
|
||||
{
|
||||
name: "fail/target-template",
|
||||
fields: fields{
|
||||
OIDC: &OIDCOptions{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "https://example.com",
|
||||
},
|
||||
Config: &Config{},
|
||||
},
|
||||
DPOP: &DPOPOptions{
|
||||
SigningKey: key,
|
||||
Target: "{{}",
|
||||
},
|
||||
},
|
||||
expectedErr: errors.New(`failed initializing DPoP options: failed parsing DPoP template: template: DeviceID:1: unexpected "}" in command`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &Options{
|
||||
OIDC: tt.fields.OIDC,
|
||||
DPOP: tt.fields.DPOP,
|
||||
}
|
||||
err := o.Validate()
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue