You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2108 lines
75 KiB
Go
2108 lines
75 KiB
Go
package acme
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/smallstep/certificates/acme/wire"
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
|
wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.step.sm/crypto/jose"
|
|
"go.step.sm/crypto/pemutil"
|
|
)
|
|
|
|
func Test_wireDPOP01Validate(t *testing.T) {
|
|
fakeKey := `-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
|
|
-----END PUBLIC KEY-----`
|
|
type test struct {
|
|
ch *Challenge
|
|
jwk *jose.JSONWebKey
|
|
db DB
|
|
payload []byte
|
|
ctx context.Context
|
|
expectedErr *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
"fail/no-provisioner": func(t *testing.T) test {
|
|
return test{
|
|
ctx: context.Background(),
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New("missing provisioner"),
|
|
},
|
|
}
|
|
},
|
|
"fail/unmarshal": func(t *testing.T) test {
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{}))
|
|
return test{
|
|
ctx: ctx,
|
|
payload: []byte("?!"),
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: "1234",
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:malformed",
|
|
Detail: "The request message was malformed",
|
|
Status: 400,
|
|
Err: errors.New(`error unmarshalling Wire DPoP challenge payload: invalid character '?' looking for beginning of value`),
|
|
},
|
|
}
|
|
},
|
|
"fail/wire-parse-id": func(t *testing.T) test {
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{}))
|
|
return test{
|
|
ctx: ctx,
|
|
payload: []byte("{}"),
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: "1234",
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`error unmarshalling challenge data: json: cannot unmarshal number into Go value of type wire.ID`),
|
|
},
|
|
}
|
|
},
|
|
"fail/wire-parse-client-id": func(t *testing.T) test {
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{}))
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
return test{
|
|
ctx: ctx,
|
|
payload: []byte("{}"),
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`error parsing device id: invalid Wire client ID username "594930e9d50bb175"`),
|
|
},
|
|
}
|
|
},
|
|
"fail/no-wire-options": func(t *testing.T) test {
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{}))
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
return test{
|
|
ctx: ctx,
|
|
payload: []byte("{}"),
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`failed getting Wire options: no Wire options available`),
|
|
},
|
|
}
|
|
},
|
|
"fail/parse-and-verify": func(t *testing.T) test {
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "http://issuerexample.com",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
Target: "{{ .DeviceID }}",
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
jwk, _ := mustAccountAndKeyAuthorization(t, "token")
|
|
return test{
|
|
ctx: ctx,
|
|
payload: []byte("{}"),
|
|
jwk: jwk,
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error {
|
|
assert.Equal(t, "chID", ch.ID)
|
|
assert.Equal(t, "azID", ch.AuthorizationID)
|
|
assert.Equal(t, "accID", ch.AccountID)
|
|
assert.Equal(t, "token", ch.Token)
|
|
assert.Equal(t, ChallengeType("wire-dpop-01"), ch.Type)
|
|
assert.Equal(t, StatusInvalid, ch.Status)
|
|
assert.Equal(t, string(valueBytes), ch.Value)
|
|
if assert.NotNil(t, ch.Error) {
|
|
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
|
|
if errors.As(ch.Error, &k) {
|
|
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
|
|
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
|
|
assert.Equal(t, 400, k.Status)
|
|
assert.Equal(t, `failed validating Wire access token: failed parsing token: go-jose/go-jose: compact JWS format must have three parts`, k.Err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"fail/db.UpdateChallenge": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?
|
|
dpopSigner, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
|
Key: jwk,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)
|
|
require.NoError(t, err)
|
|
signerPEMBytes := pem.EncodeToMemory(signerPEMBlock)
|
|
dpopBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Challenge string `json:"chal,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
},
|
|
Challenge: "token",
|
|
Handle: "wireapp://%40alice_wire@wire.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://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
APIVersion: 5,
|
|
Scope: "wire_client_id",
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
accessToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
AccessToken string `json:"access_token"`
|
|
}{
|
|
AccessToken: accessToken,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "http://issuerexample.com",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: signerPEMBytes,
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return errors.New("fail")
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`error updating challenge: fail`),
|
|
},
|
|
}
|
|
},
|
|
"fail/db.GetAllOrdersByAccountID": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?
|
|
dpopSigner, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
|
Key: jwk,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)
|
|
require.NoError(t, err)
|
|
signerPEMBytes := pem.EncodeToMemory(signerPEMBlock)
|
|
dpopBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Challenge string `json:"chal,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
},
|
|
Challenge: "token",
|
|
Handle: "wireapp://%40alice_wire@wire.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://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
APIVersion: 5,
|
|
Scope: "wire_client_id",
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
accessToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
AccessToken string `json:"access_token"`
|
|
}{
|
|
AccessToken: accessToken,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "http://issuerexample.com",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: signerPEMBytes,
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return nil
|
|
},
|
|
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
|
|
assert.Equal(t, "accID", accountID)
|
|
return nil, errors.New("fail")
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`could not find current order by account id: fail`),
|
|
},
|
|
}
|
|
},
|
|
"fail/db.GetAllOrdersByAccountID-zero": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?
|
|
dpopSigner, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
|
Key: jwk,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)
|
|
require.NoError(t, err)
|
|
signerPEMBytes := pem.EncodeToMemory(signerPEMBlock)
|
|
dpopBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Challenge string `json:"chal,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
},
|
|
Challenge: "token",
|
|
Handle: "wireapp://%40alice_wire@wire.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://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
APIVersion: 5,
|
|
Scope: "wire_client_id",
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
accessToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
AccessToken string `json:"access_token"`
|
|
}{
|
|
AccessToken: accessToken,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "http://issuerexample.com",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: signerPEMBytes,
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return nil
|
|
},
|
|
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
|
|
assert.Equal(t, "accID", accountID)
|
|
return []string{}, nil
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`there are not enough orders for this account for this custom OIDC challenge`),
|
|
},
|
|
}
|
|
},
|
|
"fail/db.CreateDpopToken": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?
|
|
dpopSigner, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
|
Key: jwk,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)
|
|
require.NoError(t, err)
|
|
signerPEMBytes := pem.EncodeToMemory(signerPEMBlock)
|
|
dpopBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Challenge string `json:"chal,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
},
|
|
Challenge: "token",
|
|
Handle: "wireapp://%40alice_wire@wire.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://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
APIVersion: 5,
|
|
Scope: "wire_client_id",
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
accessToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
AccessToken string `json:"access_token"`
|
|
}{
|
|
AccessToken: accessToken,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "http://issuerexample.com",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: signerPEMBytes,
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return nil
|
|
},
|
|
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
|
|
assert.Equal(t, "accID", accountID)
|
|
return []string{"orderID"}, nil
|
|
},
|
|
MockCreateDpopToken: func(ctx context.Context, orderID string, dpop map[string]interface{}) error {
|
|
assert.Equal(t, "orderID", orderID)
|
|
assert.Equal(t, "token", dpop["chal"].(string))
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", dpop["handle"].(string))
|
|
assert.Equal(t, "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", dpop["sub"].(string))
|
|
return errors.New("fail")
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`failed storing DPoP token: fail`),
|
|
},
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?
|
|
dpopSigner, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
|
Key: jwk,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)
|
|
require.NoError(t, err)
|
|
signerPEMBytes := pem.EncodeToMemory(signerPEMBlock)
|
|
dpopBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Challenge string `json:"chal,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
},
|
|
Challenge: "token",
|
|
Handle: "wireapp://%40alice_wire@wire.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://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
APIVersion: 5,
|
|
Scope: "wire_client_id",
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
accessToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
AccessToken string `json:"access_token"`
|
|
}{
|
|
AccessToken: accessToken,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "http://issuerexample.com",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: signerPEMBytes,
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-dpop-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return nil
|
|
},
|
|
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
|
|
assert.Equal(t, "accID", accountID)
|
|
return []string{"orderID"}, nil
|
|
},
|
|
MockCreateDpopToken: func(ctx context.Context, orderID string, dpop map[string]interface{}) error {
|
|
assert.Equal(t, "orderID", orderID)
|
|
assert.Equal(t, "token", dpop["chal"].(string))
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", dpop["handle"].(string))
|
|
assert.Equal(t, "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", dpop["sub"].(string))
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
err := wireDPOP01Validate(tc.ctx, tc.ch, tc.db, tc.jwk, tc.payload)
|
|
if tc.expectedErr != nil {
|
|
var k *Error
|
|
if errors.As(err, &k) {
|
|
assert.Equal(t, tc.expectedErr.Type, k.Type)
|
|
assert.Equal(t, tc.expectedErr.Detail, k.Detail)
|
|
assert.Equal(t, tc.expectedErr.Status, k.Status)
|
|
assert.Equal(t, tc.expectedErr.Err.Error(), k.Err.Error())
|
|
} else {
|
|
assert.Fail(t, "unexpected error type")
|
|
}
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_wireOIDC01Validate(t *testing.T) {
|
|
fakeKey := `-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
|
|
-----END PUBLIC KEY-----`
|
|
type test struct {
|
|
ch *Challenge
|
|
jwk *jose.JSONWebKey
|
|
db DB
|
|
payload []byte
|
|
srv *httptest.Server
|
|
ctx context.Context
|
|
expectedErr *Error
|
|
}
|
|
tests := map[string]func(t *testing.T) test{
|
|
"fail/no-provisioner": func(t *testing.T) test {
|
|
return test{
|
|
ctx: context.Background(),
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New("missing provisioner"),
|
|
},
|
|
}
|
|
},
|
|
"fail/unmarshal": func(t *testing.T) test {
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{}))
|
|
return test{
|
|
ctx: ctx,
|
|
payload: []byte("?!"),
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: "1234",
|
|
},
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error {
|
|
assert.Equal(t, "chID", ch.ID)
|
|
return nil
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:malformed",
|
|
Detail: "The request message was malformed",
|
|
Status: 400,
|
|
Err: errors.New(`error unmarshalling Wire OIDC challenge payload: invalid character '?' looking for beginning of value`),
|
|
},
|
|
}
|
|
},
|
|
"fail/wire-parse-id": func(t *testing.T) test {
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{}))
|
|
return test{
|
|
ctx: ctx,
|
|
payload: []byte("{}"),
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: "1234",
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`error unmarshalling challenge data: json: cannot unmarshal number into Go value of type wire.ID`),
|
|
},
|
|
}
|
|
},
|
|
"fail/no-wire-options": func(t *testing.T) test {
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{}))
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
return test{
|
|
ctx: ctx,
|
|
payload: []byte("{}"),
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`failed getting Wire options: no Wire options available`),
|
|
},
|
|
}
|
|
},
|
|
"fail/keyauth-mismatch": func(t *testing.T) test {
|
|
jwk, _ := mustAccountAndKeyAuthorization(t, "token")
|
|
payload, err := json.Marshal(struct {
|
|
IDToken string `json:"id_token"`
|
|
KeyAuth string `json:"keyauth"`
|
|
}{
|
|
IDToken: "some-token",
|
|
KeyAuth: "wrong-key-authorization",
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "http://issuer.example.com",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusInvalid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
if assert.NotNil(t, updch.Error) {
|
|
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
|
|
if errors.As(updch.Error, &k) {
|
|
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
|
|
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
|
|
assert.Equal(t, 400, k.Status)
|
|
assert.Contains(t, k.Err.Error(), `keyAuthorization does not match; expected`)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"fail/verify": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
anotherSignerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
srv := mustJWKServer(t, anotherSignerJWK.Public())
|
|
tokenBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Name string `json:"name,omitempty"`
|
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Issuer: srv.URL,
|
|
Audience: []string{"test"},
|
|
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
|
},
|
|
Name: "Alice Smith",
|
|
PreferredUsername: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
idToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
IDToken string `json:"id_token"`
|
|
KeyAuth string `json:"keyauth"`
|
|
}{
|
|
IDToken: idToken,
|
|
KeyAuth: keyAuth,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: srv.URL,
|
|
JWKSURL: srv.URL + "/keys",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
srv: srv,
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusInvalid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
if assert.NotNil(t, updch.Error) {
|
|
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
|
|
if errors.As(updch.Error, &k) {
|
|
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
|
|
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
|
|
assert.Equal(t, 400, k.Status)
|
|
assert.Equal(t, `error verifying ID token signature: failed to verify signature: failed to verify id token signature`, k.Err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"fail/validateWireOIDCClaims": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
srv := mustJWKServer(t, signerJWK.Public())
|
|
tokenBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Name string `json:"name,omitempty"`
|
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Issuer: srv.URL,
|
|
Audience: []string{"test"},
|
|
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
|
},
|
|
Name: "Alice Smith",
|
|
PreferredUsername: "wireapp://%40bob@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
idToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
IDToken string `json:"id_token"`
|
|
KeyAuth string `json:"keyauth"`
|
|
}{
|
|
IDToken: idToken,
|
|
KeyAuth: keyAuth,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: srv.URL,
|
|
JWKSURL: srv.URL + "/keys",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
srv: srv,
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusInvalid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
if assert.NotNil(t, updch.Error) {
|
|
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
|
|
if errors.As(updch.Error, &k) {
|
|
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
|
|
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
|
|
assert.Equal(t, 400, k.Status)
|
|
assert.Equal(t, `claims in OIDC ID token don't match: invalid 'handle' "wireapp://%40bob@wire.com" after transformation`, k.Err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"fail/db.UpdateChallenge": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
srv := mustJWKServer(t, signerJWK.Public())
|
|
tokenBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Name string `json:"name,omitempty"`
|
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Issuer: srv.URL,
|
|
Audience: []string{"test"},
|
|
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
|
},
|
|
Name: "Alice Smith",
|
|
PreferredUsername: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
idToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
IDToken string `json:"id_token"`
|
|
KeyAuth string `json:"keyauth"`
|
|
}{
|
|
IDToken: idToken,
|
|
KeyAuth: keyAuth,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: srv.URL,
|
|
JWKSURL: srv.URL + "/keys",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
srv: srv,
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return errors.New("fail")
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`error updating challenge: fail`),
|
|
},
|
|
}
|
|
},
|
|
"fail/db.GetAllOrdersByAccountID": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
srv := mustJWKServer(t, signerJWK.Public())
|
|
tokenBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Name string `json:"name,omitempty"`
|
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Issuer: srv.URL,
|
|
Audience: []string{"test"},
|
|
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
|
},
|
|
Name: "Alice Smith",
|
|
PreferredUsername: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
idToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
IDToken string `json:"id_token"`
|
|
KeyAuth string `json:"keyauth"`
|
|
}{
|
|
IDToken: idToken,
|
|
KeyAuth: keyAuth,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: srv.URL,
|
|
JWKSURL: srv.URL + "/keys",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
srv: srv,
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return nil
|
|
},
|
|
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
|
|
assert.Equal(t, "accID", accountID)
|
|
return nil, errors.New("fail")
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`could not retrieve current order by account id: fail`),
|
|
},
|
|
}
|
|
},
|
|
"fail/db.GetAllOrdersByAccountID-zero": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
srv := mustJWKServer(t, signerJWK.Public())
|
|
tokenBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Name string `json:"name,omitempty"`
|
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Issuer: srv.URL,
|
|
Audience: []string{"test"},
|
|
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
|
},
|
|
Name: "Alice Smith",
|
|
PreferredUsername: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
idToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
IDToken string `json:"id_token"`
|
|
KeyAuth string `json:"keyauth"`
|
|
}{
|
|
IDToken: idToken,
|
|
KeyAuth: keyAuth,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: srv.URL,
|
|
JWKSURL: srv.URL + "/keys",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
srv: srv,
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return nil
|
|
},
|
|
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
|
|
assert.Equal(t, "accID", accountID)
|
|
return []string{}, nil
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`there are not enough orders for this account for this custom OIDC challenge`),
|
|
},
|
|
}
|
|
},
|
|
"fail/db.CreateOidcToken": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
srv := mustJWKServer(t, signerJWK.Public())
|
|
tokenBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Name string `json:"name,omitempty"`
|
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Issuer: srv.URL,
|
|
Audience: []string{"test"},
|
|
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
|
},
|
|
Name: "Alice Smith",
|
|
PreferredUsername: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
idToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
IDToken string `json:"id_token"`
|
|
KeyAuth string `json:"keyauth"`
|
|
}{
|
|
IDToken: idToken,
|
|
KeyAuth: keyAuth,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: srv.URL,
|
|
JWKSURL: srv.URL + "/keys",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
srv: srv,
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return nil
|
|
},
|
|
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
|
|
assert.Equal(t, "accID", accountID)
|
|
return []string{"orderID"}, nil
|
|
},
|
|
MockCreateOidcToken: func(ctx context.Context, orderID string, idToken map[string]interface{}) error {
|
|
assert.Equal(t, "orderID", orderID)
|
|
assert.Equal(t, "Alice Smith", idToken["name"].(string))
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", idToken["handle"].(string))
|
|
return errors.New("fail")
|
|
},
|
|
},
|
|
expectedErr: &Error{
|
|
Type: "urn:ietf:params:acme:error:serverInternal",
|
|
Detail: "The server experienced an internal error",
|
|
Status: 500,
|
|
Err: errors.New(`failed storing OIDC id token: fail`),
|
|
},
|
|
}
|
|
},
|
|
"ok/wire-oidc-01": func(t *testing.T) test {
|
|
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
|
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
require.NoError(t, err)
|
|
signer, err := jose.NewSigner(jose.SigningKey{
|
|
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
|
|
Key: signerJWK,
|
|
}, new(jose.SignerOptions))
|
|
require.NoError(t, err)
|
|
srv := mustJWKServer(t, signerJWK.Public())
|
|
tokenBytes, err := json.Marshal(struct {
|
|
jose.Claims
|
|
Name string `json:"name,omitempty"`
|
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
}{
|
|
Claims: jose.Claims{
|
|
Issuer: srv.URL,
|
|
Audience: []string{"test"},
|
|
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
|
},
|
|
Name: "Alice Smith",
|
|
PreferredUsername: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
signed, err := signer.Sign(tokenBytes)
|
|
require.NoError(t, err)
|
|
idToken, err := signed.CompactSerialize()
|
|
require.NoError(t, err)
|
|
payload, err := json.Marshal(struct {
|
|
IDToken string `json:"id_token"`
|
|
KeyAuth string `json:"keyauth"`
|
|
}{
|
|
IDToken: idToken,
|
|
KeyAuth: keyAuth,
|
|
})
|
|
require.NoError(t, err)
|
|
valueBytes, err := json.Marshal(struct {
|
|
Name string `json:"name,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientID string `json:"client-id,omitempty"`
|
|
Handle string `json:"handle,omitempty"`
|
|
}{
|
|
Name: "Alice Smith",
|
|
Domain: "wire.com",
|
|
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
})
|
|
require.NoError(t, err)
|
|
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
|
|
Wire: &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: srv.URL,
|
|
JWKSURL: srv.URL + "/keys",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "test",
|
|
SignatureAlgorithms: []string{"ES256"},
|
|
SkipClientIDCheck: false,
|
|
SkipExpiryCheck: false,
|
|
SkipIssuerCheck: false,
|
|
InsecureSkipSignatureCheck: false,
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: "",
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
},
|
|
}))
|
|
return test{
|
|
ch: &Challenge{
|
|
ID: "chID",
|
|
AuthorizationID: "azID",
|
|
AccountID: "accID",
|
|
Token: "token",
|
|
Type: "wire-oidc-01",
|
|
Status: StatusPending,
|
|
Value: string(valueBytes),
|
|
},
|
|
srv: srv,
|
|
payload: payload,
|
|
ctx: ctx,
|
|
jwk: jwk,
|
|
db: &MockDB{
|
|
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
|
assert.Equal(t, "chID", updch.ID)
|
|
assert.Equal(t, "token", updch.Token)
|
|
assert.Equal(t, StatusValid, updch.Status)
|
|
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
|
|
assert.Equal(t, string(valueBytes), updch.Value)
|
|
return nil
|
|
},
|
|
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
|
|
assert.Equal(t, "accID", accountID)
|
|
return []string{"orderID"}, nil
|
|
},
|
|
MockCreateOidcToken: func(ctx context.Context, orderID string, idToken map[string]interface{}) error {
|
|
assert.Equal(t, "orderID", orderID)
|
|
assert.Equal(t, "Alice Smith", idToken["name"].(string))
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", idToken["handle"].(string))
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|
|
for name, run := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := run(t)
|
|
if tc.srv != nil {
|
|
defer tc.srv.Close()
|
|
}
|
|
err := wireOIDC01Validate(tc.ctx, tc.ch, tc.db, tc.jwk, tc.payload)
|
|
if tc.expectedErr != nil {
|
|
var k *Error
|
|
if errors.As(err, &k) {
|
|
assert.Equal(t, tc.expectedErr.Type, k.Type)
|
|
assert.Equal(t, tc.expectedErr.Detail, k.Detail)
|
|
assert.Equal(t, tc.expectedErr.Status, k.Status)
|
|
assert.Equal(t, tc.expectedErr.Err.Error(), k.Err.Error())
|
|
} else {
|
|
assert.Fail(t, "unexpected error type")
|
|
}
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_parseAndVerifyWireAccessToken(t *testing.T) {
|
|
key := `
|
|
-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEAB2IYqBWXAouDt3WcCZgCM3t9gumMEKMlgMsGenSu+fA=
|
|
-----END PUBLIC KEY-----`
|
|
publicKey, err := pemutil.Parse([]byte(key))
|
|
require.NoError(t, err)
|
|
issuer := "http://wire.com:19983/clients/7a41cf5b79683410/access-token"
|
|
wireID := wire.ID{
|
|
ClientID: "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
}
|
|
|
|
token := `eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IkIySVlxQldYQW91RHQzV2NDWmdDTTN0OWd1bU1FS01sZ01zR2VuU3UtZkEifX0.eyJpYXQiOjE3MDQ5ODUyMDUsImV4cCI6MTcwNDk4OTE2NSwibmJmIjoxNzA0OTg1MjA1LCJpc3MiOiJodHRwOi8vd2lyZS5jb206MTk5ODMvY2xpZW50cy83YTQxY2Y1Yjc5NjgzNDEwL2FjY2Vzcy10b2tlbiIsInN1YiI6IndpcmVhcHA6Ly9ndVZYNXhlRlMzZVRhdG1YQkl5QTRBITdhNDFjZjViNzk2ODM0MTBAd2lyZS5jb20iLCJhdWQiOiJodHRwOi8vd2lyZS5jb206MTk5ODMvY2xpZW50cy83YTQxY2Y1Yjc5NjgzNDEwL2FjY2Vzcy10b2tlbiIsImp0aSI6IjQyYzQ2ZDRjLWU1MTAtNDE3NS05ZmI1LWQwNTVlMTI1YTQ5ZCIsIm5vbmNlIjoiVUVKeVIyZHFPRWh6WkZKRVlXSkJhVGt5T0RORVlURTJhRXMwZEhJeGNFYyIsImNoYWwiOiJiWFVHTnBVZmNSeDNFaEIzNHhQM3k2MmFRWm9HWlM2aiIsImNuZiI6eyJraWQiOiJvTVdmTkRKUXNJNWNQbFhONVVvQk5uY0t0YzRmMmRxMnZ3Q2pqWHNxdzdRIn0sInByb29mIjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltUndiM0FyYW5kMElpd2lhbmRySWpwN0ltdDBlU0k2SWs5TFVDSXNJbU55ZGlJNklrVmtNalUxTVRraUxDSjRJam9pTVV3eFpVZ3lZVFpCWjFaMmVsUndOVnBoYkV0U1puRTJjRlpRVDNSRmFrazNhRGhVVUhwQ1dVWm5UU0o5ZlEuZXlKcFlYUWlPakUzTURRNU9EVXlNRFVzSW1WNGNDSTZNVGN3TkRrNU1qUXdOU3dpYm1KbUlqb3hOekEwT1RnMU1qQTFMQ0p6ZFdJaU9pSjNhWEpsWVhCd09pOHZaM1ZXV0RWNFpVWlRNMlZVWVhSdFdFSkplVUUwUVNFM1lUUXhZMlkxWWpjNU5qZ3pOREV3UUhkcGNtVXVZMjl0SWl3aWFuUnBJam9pTldVMk5qZzBZMkl0Tm1JME9DMDBOamhrTFdJd09URXRabVl3TkdKbFpEWmxZekpsSWl3aWJtOXVZMlVpT2lKVlJVcDVVakprY1U5RmFIcGFSa3BGV1ZkS1FtRlVhM2xQUkU1RldWUkZNbUZGY3pCa1NFbDRZMFZqSWl3aWFIUnRJam9pVUU5VFZDSXNJbWgwZFNJNkltaDBkSEE2THk5M2FYSmxMbU52YlRveE9UazRNeTlqYkdsbGJuUnpMemRoTkRGalpqVmlOemsyT0RNME1UQXZZV05qWlhOekxYUnZhMlZ1SWl3aVkyaGhiQ0k2SW1KWVZVZE9jRlZtWTFKNE0wVm9Rak0wZUZBemVUWXlZVkZhYjBkYVV6WnFJaXdpYUdGdVpHeGxJam9pZDJseVpXRndjRG92THlVME1HRnNhV05sWDNkcGNtVkFkMmx5WlM1amIyMGlMQ0owWldGdElqb2lkMmx5WlNKOS52bkN1T2JURFRLVFhCYXpyX3Z2X0xyZDBZT1Rac2xteHQtM2xKNWZKSU9iRVRidUVCTGlEaS1JVWZHcFJHTm1Dbm9IZjVocHNsWW5HeFMzSjloUmVDZyIsImNsaWVudF9pZCI6IndpcmVhcHA6Ly9ndVZYNXhlRlMzZVRhdG1YQkl5QTRBITdhNDFjZjViNzk2ODM0MTBAd2lyZS5jb20iLCJhcGlfdmVyc2lvbiI6NSwic2NvcGUiOiJ3aXJlX2NsaWVudF9pZCJ9.uCVYhmvCJm7nM1NxJQKl_XZJcSqm9eFmNmbRJkA5Wpsw70ZF1YANYC9nQ91QgsnuAbaRZMJiJt3P8ZntR2ozDQ`
|
|
ch := &Challenge{
|
|
Token: "bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j",
|
|
}
|
|
|
|
issuedAtUnix, err := strconv.ParseInt("1704985205", 10, 64)
|
|
require.NoError(t, err)
|
|
issuedAt := time.Unix(issuedAtUnix, 0)
|
|
|
|
jwkBytes := []byte(`{"crv": "Ed25519", "kty": "OKP", "x": "1L1eH2a6AgVvzTp5ZalKRfq6pVPOtEjI7h8TPzBYFgM"}`)
|
|
var accountJWK jose.JSONWebKey
|
|
json.Unmarshal(jwkBytes, &accountJWK)
|
|
|
|
rawKid, err := accountJWK.Thumbprint(crypto.SHA256)
|
|
require.NoError(t, err)
|
|
accountJWK.KeyID = base64.RawURLEncoding.EncodeToString(rawKid)
|
|
|
|
at, dpop, err := parseAndVerifyWireAccessToken(wireVerifyParams{
|
|
token: token,
|
|
tokenKey: publicKey,
|
|
dpopKey: accountJWK.Public(),
|
|
dpopKeyID: accountJWK.KeyID,
|
|
issuer: issuer,
|
|
wireID: wireID,
|
|
chToken: ch.Token,
|
|
t: issuedAt.Add(1 * time.Minute), // set validation time to be one minute after issuance
|
|
})
|
|
if assert.NoError(t, err) {
|
|
// token assertions
|
|
assert.Equal(t, "42c46d4c-e510-4175-9fb5-d055e125a49d", at.ID)
|
|
assert.Equal(t, "http://wire.com:19983/clients/7a41cf5b79683410/access-token", at.Issuer)
|
|
assert.Equal(t, "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", at.Subject)
|
|
assert.Contains(t, at.Audience, "http://wire.com:19983/clients/7a41cf5b79683410/access-token")
|
|
assert.Equal(t, "bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j", at.Challenge)
|
|
assert.Equal(t, "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", at.ClientID)
|
|
assert.Equal(t, 5, at.APIVersion)
|
|
assert.Equal(t, "wire_client_id", at.Scope)
|
|
if assert.NotNil(t, at.Cnf) {
|
|
assert.Equal(t, "oMWfNDJQsI5cPlXN5UoBNncKtc4f2dq2vwCjjXsqw7Q", at.Cnf.Kid)
|
|
}
|
|
|
|
// dpop proof assertions
|
|
dt := *dpop
|
|
assert.Equal(t, "bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j", dt["chal"].(string))
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", dt["handle"].(string))
|
|
assert.Equal(t, "POST", dt["htm"].(string))
|
|
assert.Equal(t, "http://wire.com:19983/clients/7a41cf5b79683410/access-token", dt["htu"].(string))
|
|
assert.Equal(t, "5e6684cb-6b48-468d-b091-ff04bed6ec2e", dt["jti"].(string))
|
|
assert.Equal(t, "UEJyR2dqOEhzZFJEYWJBaTkyODNEYTE2aEs0dHIxcEc", dt["nonce"].(string))
|
|
assert.Equal(t, "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", dt["sub"].(string))
|
|
assert.Equal(t, "wire", dt["team"].(string))
|
|
}
|
|
}
|
|
|
|
func Test_validateWireOIDCClaims(t *testing.T) {
|
|
fakeKey := `
|
|
-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
|
|
-----END PUBLIC KEY-----`
|
|
opts := &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "http://dex:15818/dex",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "wireapp",
|
|
Now: func() time.Time {
|
|
return time.Date(2024, 1, 12, 18, 32, 41, 0, time.UTC) // (Token Expiry: 2024-01-12 21:32:42 +0100 CET)
|
|
},
|
|
InsecureSkipSignatureCheck: true,
|
|
},
|
|
TransformTemplate: `{"name": "{{ .preferred_username }}", "handle": "{{ .name }}"}`,
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
}
|
|
|
|
err := opts.Validate()
|
|
require.NoError(t, err)
|
|
|
|
idTokenString := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjZhNDZlYzQ3YTQzYWI1ZTc4NzU3MzM5NWY1MGY4ZGQ5MWI2OTM5MzcifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE1ODE4L2RleCIsInN1YiI6IkNqcDNhWEpsWVhCd09pOHZTMmh0VjBOTFpFTlRXakoyT1dWTWFHRk9XVlp6WnlFeU5UZzFNVEpoT0RRek5qTXhaV1V6UUhkcGNtVXVZMjl0RWdSc1pHRnciLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNzA1MDkxNTYyLCJpYXQiOjE3MDUwMDUxNjIsIm5vbmNlIjoib0VjUzBRQUNXLVIyZWkxS09wUmZ2QSIsImF0X2hhc2giOiJoYzk0NmFwS25FeEV5TDVlSzJZMzdRIiwiY19oYXNoIjoidmRubFp2V1d1bVd1Z2NYR1JpOU5FUSIsIm5hbWUiOiJ3aXJlYXBwOi8vJTQwYWxpY2Vfd2lyZUB3aXJlLmNvbSIsInByZWZlcnJlZF91c2VybmFtZSI6IkFsaWNlIFNtaXRoIn0.aEBhWJugBJ9J_0L_4odUCg8SR8HMXVjd__X8uZRo42BSJQQO7-wdpy0jU3S4FOX9fQKr68wD61gS_QsnhfiT7w9U36mLpxaYlNVDCYfpa-gklVFit_0mjUOukXajTLK6H527TGiSss8z22utc40ckS1SbZa2BzKu3yOcqnFHUQwQc5sLYfpRABTB6WBoYFtnWDzdpyWJDaOzz7lfKYv2JBnf9vV8u8SYm-6gNKgtiQ3UUnjhIVUjdfHet2BMvmV2ooZ8V441RULCzKKG_sWZba-D_k_TOnSholGobtUOcKHlmVlmfUe8v7kuyBdhbPcembfgViaNldLQGKZjZfgvLg`
|
|
ctx := context.Background()
|
|
o := opts.GetOIDCOptions()
|
|
c := o.GetConfig()
|
|
verifier := o.GetProvider(ctx).Verifier(c)
|
|
idToken, err := verifier.Verify(ctx, idTokenString)
|
|
require.NoError(t, err)
|
|
|
|
wireID := wire.ID{
|
|
Name: "Alice Smith",
|
|
Handle: "wireapp://%40alice_wire@wire.com",
|
|
}
|
|
|
|
got, err := validateWireOIDCClaims(o, idToken, wireID)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", got["handle"].(string))
|
|
assert.Equal(t, "Alice Smith", got["name"].(string))
|
|
assert.Equal(t, "http://dex:15818/dex", got["iss"].(string))
|
|
}
|
|
|
|
func createWireOptions(t *testing.T, transformTemplate string) *wireprovisioner.Options {
|
|
t.Helper()
|
|
fakeKey := `
|
|
-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
|
|
-----END PUBLIC KEY-----`
|
|
opts := &wireprovisioner.Options{
|
|
OIDC: &wireprovisioner.OIDCOptions{
|
|
Provider: &wireprovisioner.Provider{
|
|
IssuerURL: "https://issuer.example.com",
|
|
},
|
|
Config: &wireprovisioner.Config{
|
|
ClientID: "unit test",
|
|
Now: time.Now,
|
|
},
|
|
TransformTemplate: transformTemplate,
|
|
},
|
|
DPOP: &wireprovisioner.DPOPOptions{
|
|
SigningKey: []byte(fakeKey),
|
|
},
|
|
}
|
|
|
|
err := opts.Validate()
|
|
require.NoError(t, err)
|
|
|
|
return opts
|
|
}
|
|
|
|
func Test_idTokenTransformation(t *testing.T) {
|
|
// {"name": "wireapp://%40alice_wire@wire.com", "preferred_username": "Alice Smith", "iss": "http://dex:15818/dex", ...}
|
|
idTokenString := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjZhNDZlYzQ3YTQzYWI1ZTc4NzU3MzM5NWY1MGY4ZGQ5MWI2OTM5MzcifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE1ODE4L2RleCIsInN1YiI6IkNqcDNhWEpsWVhCd09pOHZTMmh0VjBOTFpFTlRXakoyT1dWTWFHRk9XVlp6WnlFeU5UZzFNVEpoT0RRek5qTXhaV1V6UUhkcGNtVXVZMjl0RWdSc1pHRnciLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNzA1MDkxNTYyLCJpYXQiOjE3MDUwMDUxNjIsIm5vbmNlIjoib0VjUzBRQUNXLVIyZWkxS09wUmZ2QSIsImF0X2hhc2giOiJoYzk0NmFwS25FeEV5TDVlSzJZMzdRIiwiY19oYXNoIjoidmRubFp2V1d1bVd1Z2NYR1JpOU5FUSIsIm5hbWUiOiJ3aXJlYXBwOi8vJTQwYWxpY2Vfd2lyZUB3aXJlLmNvbSIsInByZWZlcnJlZF91c2VybmFtZSI6IkFsaWNlIFNtaXRoIn0.aEBhWJugBJ9J_0L_4odUCg8SR8HMXVjd__X8uZRo42BSJQQO7-wdpy0jU3S4FOX9fQKr68wD61gS_QsnhfiT7w9U36mLpxaYlNVDCYfpa-gklVFit_0mjUOukXajTLK6H527TGiSss8z22utc40ckS1SbZa2BzKu3yOcqnFHUQwQc5sLYfpRABTB6WBoYFtnWDzdpyWJDaOzz7lfKYv2JBnf9vV8u8SYm-6gNKgtiQ3UUnjhIVUjdfHet2BMvmV2ooZ8V441RULCzKKG_sWZba-D_k_TOnSholGobtUOcKHlmVlmfUe8v7kuyBdhbPcembfgViaNldLQGKZjZfgvLg`
|
|
var claims struct {
|
|
Name string `json:"name,omitempty"`
|
|
Handle string `json:"preferred_username,omitempty"`
|
|
Issuer string `json:"iss,omitempty"`
|
|
}
|
|
|
|
idToken, err := jose.ParseSigned(idTokenString)
|
|
require.NoError(t, err)
|
|
err = idToken.UnsafeClaimsWithoutVerification(&claims)
|
|
require.NoError(t, err)
|
|
|
|
// original token contains "Alice Smith" as handle, and name as "wireapp://%40alice_wire@wire.com"
|
|
assert.Equal(t, "Alice Smith", claims.Handle)
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", claims.Name)
|
|
assert.Equal(t, "http://dex:15818/dex", claims.Issuer)
|
|
|
|
var m map[string]any
|
|
err = idToken.UnsafeClaimsWithoutVerification(&m)
|
|
require.NoError(t, err)
|
|
|
|
opts := createWireOptions(t, "") // uses default transformation template
|
|
result, err := opts.GetOIDCOptions().Transform(m)
|
|
require.NoError(t, err)
|
|
|
|
// default transformation sets preferred username to handle; name as name
|
|
assert.Equal(t, "Alice Smith", result["handle"].(string))
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", result["name"].(string))
|
|
assert.Equal(t, "http://dex:15818/dex", result["iss"].(string))
|
|
|
|
// swap the preferred_name and the name
|
|
swap := `{"name": "{{ .preferred_username }}", "handle": "{{ .name }}"}`
|
|
opts = createWireOptions(t, swap)
|
|
result, err = opts.GetOIDCOptions().Transform(m)
|
|
require.NoError(t, err)
|
|
|
|
// with the transformation, handle now contains wireapp://%40alice_wire@wire.com, name contains Alice Smith
|
|
assert.Equal(t, "wireapp://%40alice_wire@wire.com", result["handle"].(string))
|
|
assert.Equal(t, "Alice Smith", result["name"].(string))
|
|
assert.Equal(t, "http://dex:15818/dex", result["iss"].(string))
|
|
}
|