Compare commits
169 Commits
master
...
v0.25.3-rc
Author | SHA1 | Date |
---|---|---|
Herman Slatman | 1583e53cda | 4 months ago |
Herman Slatman | 755ae0b7fa | 4 months ago |
Herman Slatman | 364566bb01 | 4 months ago |
Herman Slatman | 0d4f53f5d1 | 4 months ago |
Herman Slatman | 0a97e1bd12 | 4 months ago |
Herman Slatman | aaf5a1c95d | 4 months ago |
Herman Slatman | 95fdbc18f1 | 4 months ago |
Herman Slatman | 194341e520 | 4 months ago |
Herman Slatman | 745017cf9a | 4 months ago |
Herman Slatman | 138c1013f6 | 4 months ago |
Herman Slatman | 5d7e53303b | 4 months ago |
Herman Slatman | 2e78301189 | 4 months ago |
Herman Slatman | c6a6622892 | 4 months ago |
Herman Slatman | ef657d7d2d | 4 months ago |
Herman Slatman | e153be36d1 | 4 months ago |
Herman Slatman | 37a9f36323 | 4 months ago |
Herman Slatman | 92b61915b7 | 4 months ago |
Herman Slatman | e6d9208eeb | 5 months ago |
Herman Slatman | ace27c097b | 5 months ago |
Herman Slatman | c5792392a7 | 5 months ago |
Herman Slatman | cd21f8d51f | 5 months ago |
Herman Slatman | 19feae520b | 5 months ago |
Herman Slatman | 14e8d47118 | 5 months ago |
Herman Slatman | 8a9b1b3f79 | 5 months ago |
Herman Slatman | 79943d2e5e | 5 months ago |
Herman Slatman | a0e4cba024 | 5 months ago |
Herman Slatman | 675e418fc3 | 5 months ago |
Herman Slatman | 502334fd82 | 5 months ago |
Herman Slatman | a38132aa58 | 5 months ago |
Herman Slatman | 93ba1654ea | 5 months ago |
Herman Slatman | 4d4719a48f | 5 months ago |
beltram | 9eed61a9c5 | 5 months ago |
beltram | b8eb559ee9 | 5 months ago |
beltram | a3de984ee3 | 5 months ago |
Herman Slatman | 6ee0d70bec | 5 months ago |
Herman Slatman | 7e6356ece2 | 5 months ago |
Herman Slatman | 51d1270541 | 5 months ago |
Herman Slatman | 19dbd02451 | 5 months ago |
Herman Slatman | 2f3819aa4e | 5 months ago |
Herman Slatman | 36e14de882 | 5 months ago |
Herman Slatman | f150a4f850 | 5 months ago |
Herman Slatman | f221232a80 | 5 months ago |
Herman Slatman | b9254744a2 | 5 months ago |
Herman Slatman | 0a7fe6ebe9 | 5 months ago |
Herman Slatman | 0f0f060149 | 5 months ago |
Herman Slatman | 17578b57f2 | 5 months ago |
Herman Slatman | 31bba6fbd8 | 5 months ago |
Herman Slatman | 33be5523da | 5 months ago |
Herman Slatman | 7680da7c57 | 5 months ago |
Herman Slatman | 99934ec9a3 | 5 months ago |
Herman Slatman | 37106a438a | 5 months ago |
Herman Slatman | 7520736f5b | 5 months ago |
Herman Slatman | a24b2a5c84 | 5 months ago |
Herman Slatman | 8f129a6ced | 5 months ago |
Herman Slatman | d84abac4df | 5 months ago |
Herman Slatman | a2304c8498 | 5 months ago |
Herman Slatman | c46434f6e0 | 5 months ago |
Herman Slatman | bca179d611 | 5 months ago |
Herman Slatman | 2efd1f682d | 5 months ago |
Herman Slatman | 7d5a79190d | 5 months ago |
Herman Slatman | 768a08965d | 5 months ago |
Herman Slatman | 29202eff26 | 5 months ago |
Herman Slatman | d5b0d92bce | 5 months ago |
Herman Slatman | 0ad381b092 | 5 months ago |
Herman Slatman | 2c27e865cb | 5 months ago |
Herman Slatman | 9bb1b24bf1 | 5 months ago |
Herman Slatman | 3f37feae78 | 5 months ago |
Herman Slatman | c8160caacd | 5 months ago |
Herman Slatman | 24795720e1 | 5 months ago |
Herman Slatman | 79739e5073 | 5 months ago |
Herman Slatman | 7eacb68361 | 5 months ago |
Herman Slatman | 44721a7d58 | 5 months ago |
Herman Slatman | 348363abce | 5 months ago |
Herman Slatman | 1bf807add3 | 5 months ago |
Herman Slatman | 1f5f756fce | 5 months ago |
Herman Slatman | 6ef64b6ed6 | 5 months ago |
Herman Slatman | b6fc0005d5 | 5 months ago |
Herman Slatman | b964c97750 | 5 months ago |
Herman Slatman | acad227b25 | 5 months ago |
Herman Slatman | cd9480ab14 | 5 months ago |
Herman Slatman | 897688a831 | 5 months ago |
Herman Slatman | ca8855767d | 5 months ago |
Herman Slatman | 70a2f431fa | 5 months ago |
Herman Slatman | de25740567 | 5 months ago |
Herman Slatman | c7892e9cd3 | 5 months ago |
Herman Slatman | a423151207 | 5 months ago |
Herman Slatman | ffd887f8cc | 5 months ago |
Herman Slatman | 8997ce1a1e | 5 months ago |
Herman Slatman | bf8c17e3ec | 5 months ago |
Herman Slatman | 033aef9f9d | 5 months ago |
Herman Slatman | 6a98fea1f3 | 5 months ago |
Herman Slatman | 8faf26c593 | 5 months ago |
beltram | bf5f1201ea | 5 months ago |
Herman Slatman | e2a2e00526 | 5 months ago |
Herman Slatman | 29fa6621b1 | 5 months ago |
Herman Slatman | 7a464cdb17 | 5 months ago |
Herman Slatman | 776a839a42 | 5 months ago |
Herman Slatman | f5a2f436df | 5 months ago |
Herman Slatman | eb9893bd21 | 5 months ago |
Herman Slatman | 40668ae09e | 5 months ago |
Herman Slatman | 01169b2483 | 5 months ago |
Herman Slatman | 85309bb8ec | 5 months ago |
Herman Slatman | fdea5e7db3 | 5 months ago |
Herman Slatman | c1a7acc306 | 5 months ago |
beltram | 84e9682476 | 5 months ago |
beltram | 90b5347887 | 5 months ago |
beltram | 39bf889925 | 5 months ago |
beltram | d6ceebba94 | 5 months ago |
beltram | 6ffd913e28 | 5 months ago |
beltram | 2be77385f6 | 5 months ago |
beltram | ff07fdc0fd | 5 months ago |
beltram | 13df461e97 | 5 months ago |
beltram | 83f76433a8 | 5 months ago |
beltram | 8fd0192da3 | 5 months ago |
beltram | 4d028f7813 | 5 months ago |
beltram | ed2bce9a3c | 5 months ago |
beltram | 5fdf036a4d | 5 months ago |
beltram | 9d5c974f44 | 5 months ago |
beltram | 1b32957ff6 | 5 months ago |
Herman Slatman | ab9e1ddb28 | 5 months ago |
beltram | 7b5740153d | 5 months ago |
beltram | f5b346ee36 | 5 months ago |
beltram | 03dbd91418 | 5 months ago |
beltram | 613e6cae6e | 5 months ago |
Herman Slatman | 0b68e1bbcf | 5 months ago |
beltram | 8888262e45 | 5 months ago |
beltram | 0bc530c98e | 5 months ago |
beltram | 2e128056dc | 5 months ago |
Herman Slatman | 1a711e1b91 | 5 months ago |
beltram | abe86002ee | 5 months ago |
beltram | 76dfcb00e4 | 5 months ago |
beltram | a32bb66e47 | 5 months ago |
beltram | ff41a1193d | 5 months ago |
Stefan Berthold | 5ceed08ae0 | 5 months ago |
Stefan Berthold | 83ba0bdc51 | 5 months ago |
beltram | c4fb19d01f | 5 months ago |
beltram | 2b1223a080 | 5 months ago |
beltram | 036a144e09 | 5 months ago |
beltram | 97002040a5 | 5 months ago |
beltram | d32a3e23f0 | 5 months ago |
beltram | b58de27675 | 5 months ago |
beltram | 7c9f8020d5 | 5 months ago |
beltram | 680b6ea08f | 5 months ago |
beltram | a97991aa83 | 5 months ago |
beltram | 49ad2d9967 | 5 months ago |
beltram | a49966f4c9 | 5 months ago |
beltram | 3576cc30c8 | 5 months ago |
beltram | 4172b69816 | 5 months ago |
beltram | 79501df5a2 | 5 months ago |
beltram | 3f474f77d4 | 5 months ago |
beltram | b6ec4422b4 | 5 months ago |
Stefan Berthold | af31a167c6 | 5 months ago |
beltram | 01ef526d08 | 5 months ago |
beltram | cc5fd0a6a5 | 5 months ago |
beltram | b3dd169190 | 5 months ago |
beltram | 3eb0ff43c0 | 5 months ago |
beltram | c41a99ad75 | 5 months ago |
beltram | 5ba0ab3e44 | 5 months ago |
beltram | 73ec6c89d0 | 5 months ago |
beltram | ca01c74333 | 5 months ago |
beltram | 74ddad69dc | 5 months ago |
beltram | 83f6be1f58 | 5 months ago |
Stefan Berthold | 2208b03744 | 5 months ago |
beltram | 1fe61bee7b | 5 months ago |
Stefan Berthold | e6dd211637 | 5 months ago |
beltram | 227e932624 | 5 months ago |
Stefan Berthold | 5ca744567c | 5 months ago |
Stefan Berthold | da1e64aa53 | 5 months ago |
Stefan Berthold | 8e0e35532c | 5 months ago |
@ -0,0 +1,615 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/acme/db/nosql"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/authority/provisioner/wire"
|
||||
nosqlDB "github.com/smallstep/nosql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/minica"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "test.ca.smallstep.com"
|
||||
linkerPrefix = "acme"
|
||||
)
|
||||
|
||||
func newWireProvisionerWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME {
|
||||
t.Helper()
|
||||
prov := &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "test@acme-<test>provisioner.com",
|
||||
Options: options,
|
||||
Challenges: []provisioner.ACMEChallenge{
|
||||
provisioner.WIREOIDC_01,
|
||||
provisioner.WIREDPOP_01,
|
||||
},
|
||||
}
|
||||
|
||||
err := prov.Init(provisioner.Config{
|
||||
Claims: config.GlobalProvisionerClaims,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return prov
|
||||
}
|
||||
|
||||
// TODO(hs): replace with test CA server + acmez based test client for
|
||||
// more realistic integration test?
|
||||
func TestWireIntegration(t *testing.T) {
|
||||
accessTokenSignerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
accessTokenSignerPEMBlock, err := pemutil.Serialize(accessTokenSignerJWK.Public().Key)
|
||||
require.NoError(t, err)
|
||||
accessTokenSignerPEMBytes := pem.EncodeToMemory(accessTokenSignerPEMBlock)
|
||||
|
||||
accessTokenSigner, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(accessTokenSignerJWK.Algorithm),
|
||||
Key: accessTokenSignerJWK,
|
||||
}, new(jose.SignerOptions))
|
||||
require.NoError(t, err)
|
||||
|
||||
oidcTokenSignerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
oidcTokenSigner, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(oidcTokenSignerJWK.Algorithm),
|
||||
Key: oidcTokenSignerJWK,
|
||||
}, new(jose.SignerOptions))
|
||||
require.NoError(t, err)
|
||||
|
||||
prov := newWireProvisionerWithOptions(t, &provisioner.Options{
|
||||
X509: &provisioner.X509Options{
|
||||
Template: `{
|
||||
"subject": {
|
||||
"organization": "WireTest",
|
||||
"commonName": {{ toJson .Oidc.name }}
|
||||
},
|
||||
"uris": [{{ toJson .Oidc.preferred_username }}, {{ toJson .Dpop.sub }}],
|
||||
"keyUsage": ["digitalSignature"],
|
||||
"extKeyUsage": ["clientAuth"]
|
||||
}`,
|
||||
},
|
||||
Wire: &wire.Options{
|
||||
OIDC: &wire.OIDCOptions{
|
||||
Provider: &wire.Provider{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
AuthURL: "",
|
||||
TokenURL: "",
|
||||
JWKSURL: "",
|
||||
UserInfoURL: "",
|
||||
Algorithms: []string{"ES256"},
|
||||
},
|
||||
Config: &wire.Config{
|
||||
ClientID: "integration test",
|
||||
SignatureAlgorithms: []string{"ES256"},
|
||||
SkipClientIDCheck: true,
|
||||
SkipExpiryCheck: true,
|
||||
SkipIssuerCheck: true,
|
||||
InsecureSkipSignatureCheck: true, // NOTE: this skips actual token verification
|
||||
Now: time.Now,
|
||||
},
|
||||
TransformTemplate: "",
|
||||
},
|
||||
DPOP: &wire.DPOPOptions{
|
||||
SigningKey: accessTokenSignerPEMBytes,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// mock provisioner and linker
|
||||
ctx := context.Background()
|
||||
ctx = acme.NewProvisionerContext(ctx, prov)
|
||||
ctx = acme.NewLinkerContext(ctx, acme.NewLinker(baseURL, linkerPrefix))
|
||||
|
||||
// create temporary BoltDB file
|
||||
file, err := os.CreateTemp(os.TempDir(), "integration-db-")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("database file name:", file.Name())
|
||||
dbFn := file.Name()
|
||||
err = file.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// open BoltDB
|
||||
rawDB, err := nosqlDB.New(nosqlDB.BBoltDriver, dbFn)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create tables
|
||||
db, err := nosql.New(rawDB)
|
||||
require.NoError(t, err)
|
||||
|
||||
// make DB available to handlers
|
||||
ctx = acme.NewDatabaseContext(ctx, db)
|
||||
|
||||
// simulate signed payloads by making the signing key available in ctx
|
||||
jwk, err := jose.GenerateJWK("OKP", "", "EdDSA", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
ed25519PrivKey, ok := jwk.Key.(ed25519.PrivateKey)
|
||||
require.True(t, ok)
|
||||
|
||||
dpopSigner, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
||||
Key: jwk,
|
||||
}, new(jose.SignerOptions))
|
||||
require.NoError(t, err)
|
||||
|
||||
ed25519PubKey, ok := ed25519PrivKey.Public().(ed25519.PublicKey)
|
||||
require.True(t, ok)
|
||||
|
||||
jwk.Key = ed25519PubKey
|
||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||
|
||||
// get directory
|
||||
dir := func(ctx context.Context) (dir Directory) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/foo/bar", http.NoBody)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
GetDirectory(w, req)
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("directory:", dir)
|
||||
|
||||
// get nonce
|
||||
nonce := func(ctx context.Context) (nonce string) {
|
||||
req := httptest.NewRequest(http.MethodGet, dir.NewNonce, http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
addNonce(GetNonce)(w, req)
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
||||
|
||||
nonce = res.Header["Replay-Nonce"][0]
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("nonce:", nonce)
|
||||
|
||||
// create new account
|
||||
acc := func(ctx context.Context) (acc *acme.Account) {
|
||||
// create payload
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
rawNar, err := json.Marshal(nar)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create account
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: rawNar})
|
||||
req := httptest.NewRequest(http.MethodGet, dir.NewAccount, http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
NewAccount(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusCreated, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
locationParts := strings.Split(res.Header["Location"][0], "/")
|
||||
acc, err = db.GetAccount(ctx, locationParts[len(locationParts)-1])
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
t.Log("account ID:", acc.ID)
|
||||
|
||||
// new order
|
||||
order := func(ctx context.Context) (order *acme.Order) {
|
||||
mockMustAuthority(t, &mockCA{})
|
||||
nor := &NewOrderRequest{
|
||||
Identifiers: []acme.Identifier{
|
||||
{
|
||||
Type: "wireapp-user",
|
||||
Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`,
|
||||
},
|
||||
{
|
||||
Type: "wireapp-device",
|
||||
Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(nor)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
req := httptest.NewRequest("POST", "https://random.local/", http.NoBody)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
NewOrder(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusCreated, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &order)
|
||||
require.NoError(t, err)
|
||||
|
||||
order, err = db.GetOrder(ctx, order.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("authzs IDs:", order.AuthorizationIDs)
|
||||
|
||||
// get authorization
|
||||
getAuthz := func(ctx context.Context, authzID string) (az *acme.Authorization) {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("authzID", authzID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetAuthorization(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &az)
|
||||
require.NoError(t, err)
|
||||
|
||||
az, err = db.GetAuthorization(ctx, authzID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
var azs []*acme.Authorization
|
||||
for _, azID := range order.AuthorizationIDs {
|
||||
az := getAuthz(ctx, azID)
|
||||
azs = append(azs, az)
|
||||
for _, challenge := range az.Challenges {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("chID", challenge.ID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
|
||||
var payload []byte
|
||||
switch challenge.Type {
|
||||
case acme.WIREDPOP01:
|
||||
dpopBytes, err := json.Marshal(struct {
|
||||
jose.Claims
|
||||
Challenge string `json:"chal,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
HTU string `json:"htu,omitempty"`
|
||||
}{
|
||||
Claims: jose.Claims{
|
||||
Subject: "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com",
|
||||
},
|
||||
Challenge: "token",
|
||||
Handle: "wireapp://%40alice.smith.qa@example.com",
|
||||
Nonce: "nonce",
|
||||
HTU: "http://issuer.example.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
dpop, err := dpopSigner.Sign(dpopBytes)
|
||||
require.NoError(t, err)
|
||||
proof, err := dpop.CompactSerialize()
|
||||
require.NoError(t, err)
|
||||
tokenBytes, err := json.Marshal(struct {
|
||||
jose.Claims
|
||||
Challenge string `json:"chal,omitempty"`
|
||||
Cnf struct {
|
||||
Kid string `json:"kid,omitempty"`
|
||||
} `json:"cnf"`
|
||||
Proof string `json:"proof,omitempty"`
|
||||
ClientID string `json:"client_id"`
|
||||
APIVersion int `json:"api_version"`
|
||||
Scope string `json:"scope"`
|
||||
}{
|
||||
Claims: jose.Claims{
|
||||
Issuer: "http://issuer.example.com",
|
||||
Audience: []string{"test"},
|
||||
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
||||
},
|
||||
Challenge: "token",
|
||||
Cnf: struct {
|
||||
Kid string `json:"kid,omitempty"`
|
||||
}{
|
||||
Kid: jwk.KeyID,
|
||||
},
|
||||
Proof: proof,
|
||||
ClientID: "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com",
|
||||
APIVersion: 5,
|
||||
Scope: "wire_client_id",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
signed, err := accessTokenSigner.Sign(tokenBytes)
|
||||
require.NoError(t, err)
|
||||
accessToken, err := signed.CompactSerialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
p, err := json.Marshal(struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}{
|
||||
AccessToken: accessToken,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
payload = p
|
||||
case acme.WIREOIDC01:
|
||||
keyAuth, err := acme.KeyAuthorization("token", jwk)
|
||||
require.NoError(t, err)
|
||||
tokenBytes, err := json.Marshal(struct {
|
||||
jose.Claims
|
||||
Name string `json:"name,omitempty"`
|
||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||
KeyAuth string `json:"keyauth"`
|
||||
}{
|
||||
Claims: jose.Claims{
|
||||
Issuer: "https://issuer.example.com",
|
||||
Audience: []string{"test"},
|
||||
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
||||
},
|
||||
Name: "Alice Smith",
|
||||
PreferredUsername: "wireapp://%40alice_wire@wire.com",
|
||||
KeyAuth: keyAuth,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
signed, err := oidcTokenSigner.Sign(tokenBytes)
|
||||
require.NoError(t, err)
|
||||
idToken, err := signed.CompactSerialize()
|
||||
require.NoError(t, err)
|
||||
p, err := json.Marshal(struct {
|
||||
IDToken string `json:"id_token"`
|
||||
}{
|
||||
IDToken: idToken,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
payload = p
|
||||
default:
|
||||
require.Fail(t, "unexpected challenge payload type")
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payload})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetChallenge(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close() //nolint:gocritic // close the body
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &challenge)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("challenge:", challenge.ID, challenge.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// get/validate challenge simulation
|
||||
updateAz := func(ctx context.Context, az *acme.Authorization) (updatedAz *acme.Authorization) {
|
||||
now := clock.Now().Format(time.RFC3339)
|
||||
for _, challenge := range az.Challenges {
|
||||
challenge.Status = acme.StatusValid
|
||||
challenge.ValidatedAt = now
|
||||
err := db.UpdateChallenge(ctx, challenge)
|
||||
if err != nil {
|
||||
t.Error("updating challenge", challenge.ID, ":", err)
|
||||
}
|
||||
}
|
||||
|
||||
updatedAz, err = db.GetAuthorization(ctx, az.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
for _, az := range azs {
|
||||
updatedAz := updateAz(ctx, az)
|
||||
for _, challenge := range updatedAz.Challenges {
|
||||
t.Log("updated challenge:", challenge.ID, challenge.Status)
|
||||
switch challenge.Type {
|
||||
case acme.WIREOIDC01:
|
||||
err = db.CreateOidcToken(ctx, order.ID, map[string]any{"name": "Smith, Alice M (QA)", "preferred_username": "wireapp://%40alice.smith.qa@example.com"})
|
||||
require.NoError(t, err)
|
||||
case acme.WIREDPOP01:
|
||||
err = db.CreateDpopToken(ctx, order.ID, map[string]any{"sub": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com"})
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
require.Fail(t, "unexpected challenge type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get order
|
||||
updatedOrder := func(ctx context.Context) (updatedOrder *acme.Order) {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("ordID", order.ID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetOrder(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &updatedOrder)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, acme.StatusReady, updatedOrder.Status)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("updated order status:", updatedOrder.Status)
|
||||
|
||||
// finalize order
|
||||
finalizedOrder := func(ctx context.Context) (finalizedOrder *acme.Order) {
|
||||
ca, err := minica.New(minica.WithName("WireTestCA"))
|
||||
require.NoError(t, err)
|
||||
mockMustAuthority(t, &mockCASigner{
|
||||
signer: func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
var (
|
||||
certOptions []x509util.Option
|
||||
)
|
||||
for _, op := range extraOpts {
|
||||
if k, ok := op.(provisioner.CertificateOptions); ok {
|
||||
certOptions = append(certOptions, k.Options(signOpts)...)
|
||||
}
|
||||
}
|
||||
|
||||
x509utilTemplate, err := x509util.NewCertificate(csr, certOptions...)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := x509utilTemplate.GetCertificate()
|
||||
require.NotNil(t, template)
|
||||
|
||||
cert, err := ca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
|
||||
u1, err := url.Parse("wireapp://%40alice.smith.qa@example.com")
|
||||
require.NoError(t, err)
|
||||
u2, err := url.Parse("wireapp://lJGYPz0ZRq2kvc_XpdaDlA%21ed416ce8ecdd9fad@example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*url.URL{u1, u2}, cert.URIs)
|
||||
assert.Equal(t, "Smith, Alice M (QA)", cert.Subject.CommonName)
|
||||
|
||||
return []*x509.Certificate{cert, ca.Intermediate}, nil
|
||||
},
|
||||
})
|
||||
|
||||
qUserID, err := url.Parse("wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
qUserName, err := url.Parse("wireapp://%40alice.smith.qa@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
csrTemplate := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"example.com"},
|
||||
ExtraNames: []pkix.AttributeTypeAndValue{
|
||||
{
|
||||
Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},
|
||||
Value: "Smith, Alice M (QA)",
|
||||
},
|
||||
},
|
||||
},
|
||||
URIs: []*url.URL{
|
||||
qUserName,
|
||||
qUserID,
|
||||
},
|
||||
SignatureAlgorithm: x509.PureEd25519,
|
||||
}
|
||||
|
||||
csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, priv)
|
||||
require.NoError(t, err)
|
||||
|
||||
fr := FinalizeRequest{CSR: base64.RawURLEncoding.EncodeToString(csr)}
|
||||
frRaw, err := json.Marshal(fr)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: frRaw})
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("ordID", order.ID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
FinalizeOrder(w, req)
|
||||
|
||||
res := w.Result()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(bytes.TrimSpace(body), &finalizedOrder)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, acme.StatusValid, finalizedOrder.Status)
|
||||
|
||||
finalizedOrder, err = db.GetOrder(ctx, order.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}(ctx)
|
||||
t.Log("finalized order status:", finalizedOrder.Status)
|
||||
}
|
||||
|
||||
type mockCASigner struct {
|
||||
signer func(*x509.CertificateRequest, provisioner.SignOptions, ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
}
|
||||
|
||||
func (m *mockCASigner) SignWithContext(_ context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
if m.signer == nil {
|
||||
return nil, errors.New("unimplemented")
|
||||
}
|
||||
return m.signer(cr, opts, signOpts...)
|
||||
}
|
||||
|
||||
func (m *mockCASigner) AreSANsAllowed(ctx context.Context, sans []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCASigner) IsRevoked(sn string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *mockCASigner) Revoke(ctx context.Context, opts *authority.RevokeOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCASigner) LoadProvisionerByName(string) (provisioner.Interface, error) {
|
||||
return nil, nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,121 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
type dbDpopToken struct {
|
||||
ID string `json:"id"`
|
||||
Content []byte `json:"content"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// getDBDpopToken retrieves and unmarshals an DPoP type from the database.
|
||||
func (db *DB) getDBDpopToken(_ context.Context, orderID string) (*dbDpopToken, error) {
|
||||
b, err := db.db.Get(wireDpopTokenTable, []byte(orderID))
|
||||
if err != nil {
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "dpop token %q not found", orderID)
|
||||
}
|
||||
return nil, fmt.Errorf("failed loading dpop token %q: %w", orderID, err)
|
||||
}
|
||||
|
||||
d := new(dbDpopToken)
|
||||
if err := json.Unmarshal(b, d); err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshaling dpop token %q into dbDpopToken: %w", orderID, err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// GetDpopToken retrieves an DPoP from the database.
|
||||
func (db *DB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) {
|
||||
dbDpop, err := db.getDBDpopToken(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dpop := make(map[string]any)
|
||||
err = json.Unmarshal(dbDpop.Content, &dpop)
|
||||
|
||||
return dpop, err
|
||||
}
|
||||
|
||||
// CreateDpopToken creates DPoP resources and saves them to the DB.
|
||||
func (db *DB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error {
|
||||
content, err := json.Marshal(dpop)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshaling dpop token: %w", err)
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
dbDpop := &dbDpopToken{
|
||||
ID: orderID,
|
||||
Content: content,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := db.save(ctx, orderID, dbDpop, nil, "dpop", wireDpopTokenTable); err != nil {
|
||||
return fmt.Errorf("failed saving dpop token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbOidcToken struct {
|
||||
ID string `json:"id"`
|
||||
Content []byte `json:"content"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// getDBOidcToken retrieves and unmarshals an OIDC id token type from the database.
|
||||
func (db *DB) getDBOidcToken(_ context.Context, orderID string) (*dbOidcToken, error) {
|
||||
b, err := db.db.Get(wireOidcTokenTable, []byte(orderID))
|
||||
if err != nil {
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "oidc token %q not found", orderID)
|
||||
}
|
||||
return nil, fmt.Errorf("failed loading oidc token %q: %w", orderID, err)
|
||||
}
|
||||
|
||||
o := new(dbOidcToken)
|
||||
if err := json.Unmarshal(b, o); err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshaling oidc token %q into dbOidcToken: %w", orderID, err)
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// GetOidcToken retrieves an oidc token from the database.
|
||||
func (db *DB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) {
|
||||
dbOidc, err := db.getDBOidcToken(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idToken := make(map[string]any)
|
||||
err = json.Unmarshal(dbOidc.Content, &idToken)
|
||||
|
||||
return idToken, err
|
||||
}
|
||||
|
||||
// CreateOidcToken creates oidc token resources and saves them to the DB.
|
||||
func (db *DB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error {
|
||||
content, err := json.Marshal(idToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshaling oidc token: %w", err)
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
dbOidc := &dbOidcToken{
|
||||
ID: orderID,
|
||||
Content: content,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := db.save(ctx, orderID, dbOidc, nil, "oidc", wireOidcTokenTable); err != nil {
|
||||
return fmt.Errorf("failed saving oidc token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsErrNotFound(t *testing.T) {
|
||||
type args struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"true ErrNotFound", args{ErrNotFound}, true},
|
||||
{"true sql.ErrNoRows", args{sql.ErrNoRows}, true},
|
||||
{"true wrapped ErrNotFound", args{fmt.Errorf("something failed: %w", ErrNotFound)}, true},
|
||||
{"true wrapped sql.ErrNoRows", args{fmt.Errorf("something failed: %w", sql.ErrNoRows)}, true},
|
||||
{"false other", args{errors.New("not found")}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsErrNotFound(tt.args.err); got != tt.want {
|
||||
t.Errorf("IsErrNotFound() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.step.sm/crypto/kms/uri"
|
||||
)
|
||||
|
||||
type UserID struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
type DeviceID struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
ClientID string `json:"client-id,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
func ParseUserID(value string) (id UserID, err error) {
|
||||
if err = json.Unmarshal([]byte(value), &id); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case id.Handle == "":
|
||||
err = errors.New("handle must not be empty")
|
||||
case id.Name == "":
|
||||
err = errors.New("name must not be empty")
|
||||
case id.Domain == "":
|
||||
err = errors.New("domain must not be empty")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ParseDeviceID(value string) (id DeviceID, err error) {
|
||||
if err = json.Unmarshal([]byte(value), &id); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case id.Handle == "":
|
||||
err = errors.New("handle must not be empty")
|
||||
case id.Name == "":
|
||||
err = errors.New("name must not be empty")
|
||||
case id.Domain == "":
|
||||
err = errors.New("domain must not be empty")
|
||||
case id.ClientID == "":
|
||||
err = errors.New("client-id must not be empty")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type ClientID struct {
|
||||
Scheme string
|
||||
Username string
|
||||
DeviceID string
|
||||
Domain string
|
||||
}
|
||||
|
||||
// ParseClientID parses a Wire clientID. The ClientID format is as follows:
|
||||
//
|
||||
// "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
||||
//
|
||||
// where '!' is used as a separator between the user id & device id.
|
||||
func ParseClientID(clientID string) (ClientID, error) {
|
||||
clientIDURI, err := uri.Parse(clientID)
|
||||
if err != nil {
|
||||
return ClientID{}, fmt.Errorf("invalid Wire client ID URI %q: %w", clientID, err)
|
||||
}
|
||||
if clientIDURI.Scheme != "wireapp" {
|
||||
return ClientID{}, fmt.Errorf("invalid Wire client ID scheme %q; expected \"wireapp\"", clientIDURI.Scheme)
|
||||
}
|
||||
fullUsername := clientIDURI.User.Username()
|
||||
parts := strings.SplitN(fullUsername, "!", 2)
|
||||
if len(parts) != 2 {
|
||||
return ClientID{}, fmt.Errorf("invalid Wire client ID username %q", fullUsername)
|
||||
}
|
||||
return ClientID{
|
||||
Scheme: clientIDURI.Scheme,
|
||||
Username: parts[0],
|
||||
DeviceID: parts[1],
|
||||
Domain: clientIDURI.Host,
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseUserID(t *testing.T) {
|
||||
ok := `{"name": "Alice Smith", "domain": "wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
failJSON := `{"name": }`
|
||||
emptyHandle := `{"name": "Alice Smith", "domain": "wire.com", "handle": ""}`
|
||||
emptyName := `{"name": "", "domain": "wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
emptyDomain := `{"name": "Alice Smith", "domain": "", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantWireID UserID
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "ok", value: ok, wantWireID: UserID{Name: "Alice Smith", Domain: "wire.com", Handle: "wireapp://%40alice_wire@wire.com"}},
|
||||
{name: "fail/json", value: failJSON, wantErr: true},
|
||||
{name: "fail/empty-handle", value: emptyHandle, wantErr: true},
|
||||
{name: "fail/empty-name", value: emptyName, wantErr: true},
|
||||
{name: "fail/empty-domain", value: emptyDomain, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotWireID, err := ParseUserID(tt.value)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantWireID, gotWireID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeviceID(t *testing.T) {
|
||||
ok := `{"name": "device", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
failJSON := `{"name": }`
|
||||
emptyHandle := `{"name": "device", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": ""}`
|
||||
emptyName := `{"name": "", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
emptyDomain := `{"name": "device", "domain": "", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
emptyClientID := `{"name": "device", "domain": "wire.com", "client-id": "", "handle": "wireapp://%40alice_wire@wire.com"}`
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantWireID DeviceID
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "ok", value: ok, wantWireID: DeviceID{Name: "device", Domain: "wire.com", ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", Handle: "wireapp://%40alice_wire@wire.com"}},
|
||||
{name: "fail/json", value: failJSON, wantErr: true},
|
||||
{name: "fail/empty-handle", value: emptyHandle, wantErr: true},
|
||||
{name: "fail/empty-name", value: emptyName, wantErr: true},
|
||||
{name: "fail/empty-domain", value: emptyDomain, wantErr: true},
|
||||
{name: "fail/empty-client-id", value: emptyClientID, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotWireID, err := ParseDeviceID(tt.value)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"errors"
|
||||
"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) {
|
||||
if deviceID == "" {
|
||||
return "", errors.New("deviceID must not be empty")
|
||||
}
|
||||
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,58 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDPOPOptions_EvaluateTarget(t *testing.T) {
|
||||
tu := "http://wire.com:15958/clients/{{.DeviceID}}/access-token"
|
||||
target, err := template.New("DeviceID").Parse(tu)
|
||||
require.NoError(t, err)
|
||||
fail := "https:/wire.com:15958/clients/{{.DeviceId}}/access-token"
|
||||
failTarget, err := template.New("DeviceID").Parse(fail)
|
||||
require.NoError(t, err)
|
||||
type fields struct {
|
||||
target *template.Template
|
||||
}
|
||||
type args struct {
|
||||
deviceID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "ok", fields: fields{target: target}, args: args{deviceID: "deviceID"}, want: "http://wire.com:15958/clients/deviceID/access-token",
|
||||
},
|
||||
{
|
||||
name: "fail/empty", fields: fields{target: target}, args: args{deviceID: ""}, expectedErr: errors.New("deviceID must not be empty"),
|
||||
},
|
||||
{
|
||||
name: "fail/template", fields: fields{target: failTarget}, args: args{deviceID: "bla"}, expectedErr: errors.New(`failed executing DPoP template: template: DeviceID:1:32: executing "DeviceID" at <.DeviceId>: can't evaluate field DeviceId in type struct { DeviceID string }`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &DPOPOptions{
|
||||
target: tt.fields.target,
|
||||
}
|
||||
got, err := o.EvaluateTarget(tt.args.deviceID)
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
assert.Empty(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
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 {
|
||||
DiscoveryBaseURL string `json:"discoveryBaseUrl,omitempty"`
|
||||
IssuerURL string `json:"issuerUrl,omitempty"`
|
||||
AuthURL string `json:"authorizationUrl,omitempty"`
|
||||
TokenURL string `json:"tokenUrl,omitempty"`
|
||||
JWKSURL string `json:"jwksUrl,omitempty"`
|
||||
UserInfoURL string `json:"userInfoUrl,omitempty"`
|
||||
Algorithms []string `json:"signatureAlgorithms,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"`
|
||||
|
||||
target *template.Template
|
||||
transform *template.Template
|
||||
oidcProviderConfig *oidc.ProviderConfig
|
||||
provider *oidc.Provider
|
||||
verifier *oidc.IDTokenVerifier
|
||||
}
|
||||
|
||||
func (o *OIDCOptions) GetVerifier(ctx context.Context) (*oidc.IDTokenVerifier, error) {
|
||||
if o.verifier == nil {
|
||||
switch {
|
||||
case o.Provider.DiscoveryBaseURL != "":
|
||||
// creates a new OIDC provider using automatic discovery and the default HTTP client
|
||||
provider, err := oidc.NewProvider(ctx, o.Provider.DiscoveryBaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed creating new OIDC provider using discovery: %w", err)
|
||||
}
|
||||
o.provider = provider
|
||||
default:
|
||||
o.provider = o.oidcProviderConfig.NewProvider(ctx)
|
||||
}
|
||||
|
||||
if o.provider == nil {
|
||||
return nil, errors.New("no OIDC provider available")
|
||||
}
|
||||
|
||||
o.verifier = o.provider.Verifier(o.getConfig())
|
||||
}
|
||||
|
||||
return o.verifier, nil
|
||||
}
|
||||
|
||||
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 == "" && o.Provider.DiscoveryBaseURL == "" {
|
||||
return errors.New("either OIDC discovery or issuer URL must be set")
|
||||
}
|
||||
|
||||
if o.Provider.DiscoveryBaseURL == "" {
|
||||
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,305 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.step.sm/crypto/jose"
|
||||
)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCOptions_EvaluateTarget(t *testing.T) {
|
||||
tu := "http://target.example.com/{{.DeviceID}}"
|
||||
target, err := template.New("DeviceID").Parse(tu)
|
||||
require.NoError(t, err)
|
||||
empty := "http://target.example.com"
|
||||
emptyTarget, err := template.New("DeviceID").Parse(empty)
|
||||
require.NoError(t, err)
|
||||
fail := "https:/wire.com:15958/clients/{{.DeviceId}}/access-token"
|
||||
failTarget, err := template.New("DeviceID").Parse(fail)
|
||||
require.NoError(t, err)
|
||||
type fields struct {
|
||||
target *template.Template
|
||||
}
|
||||
type args struct {
|
||||
deviceID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "ok", fields: fields{target: target}, args: args{deviceID: "deviceID"}, want: "http://target.example.com/deviceID",
|
||||
},
|
||||
{
|
||||
name: "ok/empty", fields: fields{target: emptyTarget}, args: args{deviceID: ""}, want: "http://target.example.com",
|
||||
},
|
||||
{
|
||||
name: "fail/template", fields: fields{target: failTarget}, args: args{deviceID: "bla"}, expectedErr: errors.New(`failed executing OIDC template: template: DeviceID:1:32: executing "DeviceID" at <.DeviceId>: can't evaluate field DeviceId in type struct { DeviceID string }`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &OIDCOptions{
|
||||
target: tt.fields.target,
|
||||
}
|
||||
got, err := o.EvaluateTarget(tt.args.deviceID)
|
||||
if tt.expectedErr != nil {
|
||||
assert.EqualError(t, err, tt.expectedErr.Error())
|
||||
assert.Empty(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCOptions_GetVerifier(t *testing.T) {
|
||||
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
srv := mustDiscoveryServer(t, signerJWK.Public())
|
||||
defer srv.Close()
|
||||
type fields struct {
|
||||
Provider *Provider
|
||||
Config *Config
|
||||
TransformTemplate string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
ctx context.Context
|
||||
want *oidc.IDTokenVerifier
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "fail/invalid-discovery-url",
|
||||
fields: fields{
|
||||
Provider: &Provider{
|
||||
DiscoveryBaseURL: "http://invalid.example.com",
|
||||
},
|
||||
Config: &Config{
|
||||
ClientID: "client-id",
|
||||
},
|
||||
TransformTemplate: "http://target.example.com/{{.DeviceID}}",
|
||||
},
|
||||
ctx: context.Background(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok/auto",
|
||||
fields: fields{
|
||||
Provider: &Provider{
|
||||
DiscoveryBaseURL: srv.URL,
|
||||
},
|
||||
Config: &Config{
|
||||
ClientID: "client-id",
|
||||
},
|
||||
TransformTemplate: "http://target.example.com/{{.DeviceID}}",
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
name: "ok/fixed",
|
||||
fields: fields{
|
||||
Provider: &Provider{
|
||||
IssuerURL: "http://issuer.example.com",
|
||||
},
|
||||
Config: &Config{
|
||||
ClientID: "client-id",
|
||||
},
|
||||
TransformTemplate: "http://target.example.com/{{.DeviceID}}",
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &OIDCOptions{
|
||||
Provider: tt.fields.Provider,
|
||||
Config: tt.fields.Config,
|
||||
TransformTemplate: tt.fields.TransformTemplate,
|
||||
}
|
||||
|
||||
err := o.validateAndInitialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
verifier, err := o.GetVerifier(tt.ctx)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, verifier)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, verifier)
|
||||
if assert.NotNil(t, o.provider) {
|
||||
assert.NotNil(t, o.provider.Endpoint())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustDiscoveryServer(t *testing.T, pub jose.JSONWebKey) *httptest.Server {
|
||||
t.Helper()
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
b, err := json.Marshal(struct {
|
||||
Keys []jose.JSONWebKey `json:"keys,omitempty"`
|
||||
}{
|
||||
Keys: []jose.JSONWebKey{pub},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
jwks := string(b)
|
||||
|
||||
wellKnown := fmt.Sprintf(`{
|
||||
"issuer": "%[1]s",
|
||||
"authorization_endpoint": "%[1]s/auth",
|
||||
"token_endpoint": "%[1]s/token",
|
||||
"jwks_uri": "%[1]s/keys",
|
||||
"userinfo_endpoint": "%[1]s/userinfo",
|
||||
"id_token_signing_alg_values_supported": ["ES256"]
|
||||
}`, server.URL)
|
||||
|
||||
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, req *http.Request) {
|
||||
_, err := io.WriteString(w, wellKnown)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/keys", func(w http.ResponseWriter, req *http.Request) {
|
||||
_, err := io.WriteString(w, jwks)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
})
|
||||
|
||||
t.Cleanup(server.Close)
|
||||
return server
|
||||
}
|
@ -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: either OIDC discovery or issuer URL must be set"),
|
||||
},
|
||||
{
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue