reorg packages, remove outdated server example
This commit is contained in:
committed by
Michael Muré
parent
4f4331b677
commit
4167bf44bd
@@ -1,6 +0,0 @@
|
||||
# HTTP Server middleware example
|
||||
|
||||
This package shows an example of an HTTP middleware with the following features:
|
||||
- decoding a UCAN delegation token from the `Authorization: Bearer` HTTP Header, according to the PriorityConnect Step0 way (which is not the fully correct UCAN way)
|
||||
- minimal verifications, no policies or args
|
||||
- retrieval of values passed as token metadata, and insertion in the go context
|
||||
66
toolkit/server/bearer/Readme.md
Normal file
66
toolkit/server/bearer/Readme.md
Normal file
@@ -0,0 +1,66 @@
|
||||
## Motivations
|
||||
|
||||
UCAN is normally a pure RPC construct, when the entirety of the request's parameters are part of the invocation, in the form of `args`. Those `args` are evaluated against the delegation's [policy](https://github.com/ucan-wg/delegation/tree/v1_ipld?tab=readme-ov-file#policy) to determine if the request is allowed or not, then the request handling happens purely based on those args and the `command`. In that setup, the service would have a single entry point.
|
||||
|
||||
Unfortunately, we live in a world of REST APIs, or JSON-RPC. Some adaptations or concessions need to be made.
|
||||
|
||||
In this package, we cross the chasm of the pure UCAN world into our practical needs. This can, however, be done in a reasonable way.
|
||||
|
||||
## Example
|
||||
|
||||
Below is an example of `args` in Dag-Json format, where the values are recomposed server-side from the HTTP request (header and JSONRPC body):
|
||||
|
||||
```json
|
||||
{
|
||||
"http": {
|
||||
"scheme": "https",
|
||||
"method": "POST",
|
||||
"host": "mainnet.infura.io",
|
||||
"path": ""
|
||||
},
|
||||
"jsonrpc": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "eth_blockbynumber",
|
||||
"params": [],
|
||||
"id": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
Those `args` can be evaluated against a delegation's policy, for example:
|
||||
```json
|
||||
{
|
||||
"cmd": "/infura/jsonrpc",
|
||||
"pol": [
|
||||
["==", ".http.host", "mainnet.infura.io"],
|
||||
["like", ".jsonrpc.method", "eth_*"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Security implications
|
||||
|
||||
UCAN essentially aims for perfect security. By having external args, we break that security perimeter, and we now need to arbitrate between security and practicality.
|
||||
|
||||
First, what are we breaking exactly? Normally, the invocation has all the parameters and is signed by the invoker. This means that an attacker cannot intercept (MITM) a request and change the parameters when relaying it to the server. As they are signed, that would make the request invalid.
|
||||
|
||||
If we have external args, now an attacker can intercept the request, change it, and pretend to be that person doing other things than intended. **That may of may not be an actual problem, depending on the situation.**
|
||||
|
||||
There is a way to get around that, and have the best of both worlds, but **it comes with a client side complexity**: we can hash the external values and put them into the invocation's `args`. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"http": "zQmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D",
|
||||
"jsonrpc": "zQmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9"
|
||||
}
|
||||
```
|
||||
|
||||
When the server receives the request, it can now reconstruct the values from the HTTP request, verify the hash and replace it with the real values for evaluation against the policies. **We are now back to the better security model**, but at the price of client-side complexity.
|
||||
|
||||
## API design
|
||||
|
||||
Therefore, the server-side logic is made to have this hashing optional:
|
||||
- if present, the server honors the hash and enforces the security
|
||||
- the client can opt out of passing that hash, and won't benefit from the enforced security
|
||||
|
||||
The particular hash selected is SHA2-256 of the DAG-CBOR encoded argument, expressed in the form of a Multihash in raw bytes.
|
||||
The arguments being hashed are the complete map of values, including the root key being replaced (for example `jsonrpc` or `http`).
|
||||
160
toolkit/server/bearer/http.go
Normal file
160
toolkit/server/bearer/http.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package bearer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/multiformats/go-multihash"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
)
|
||||
|
||||
// HttpArgsKey is the key in the args, used for:
|
||||
// - if it exists in the invocation, holds a hash of the args derived from the HTTP request
|
||||
// - in the final args to be evaluated against the policies, holds the args derived from the HTTP request
|
||||
const HttpArgsKey = "http"
|
||||
|
||||
type HttpBearer struct {
|
||||
pol policy.Policy
|
||||
originalArgs args.ReadOnly
|
||||
req *http.Request
|
||||
|
||||
once sync.Once
|
||||
args *args.Args
|
||||
argsIpld ipld.Node
|
||||
}
|
||||
|
||||
func NewHttpBearer(pol policy.Policy, originalArgs args.ReadOnly, req *http.Request) *HttpBearer {
|
||||
return &HttpBearer{pol: pol, originalArgs: originalArgs, req: req}
|
||||
}
|
||||
|
||||
func (hc *HttpBearer) Verify() error {
|
||||
if err := hc.makeArgs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := hc.verifyHash(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ok, leaf := hc.pol.PartialMatch(hc.argsIpld)
|
||||
if !ok {
|
||||
return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hc *HttpBearer) Args() (*args.Args, error) {
|
||||
if err := hc.makeArgs(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hc.args, nil
|
||||
}
|
||||
|
||||
func (hc *HttpBearer) makeArgs() error {
|
||||
var outerErr error
|
||||
hc.once.Do(func() {
|
||||
var err error
|
||||
hc.args, err = makeHttpArgs(hc.req)
|
||||
if err != nil {
|
||||
outerErr = err
|
||||
return
|
||||
}
|
||||
|
||||
hc.argsIpld, err = hc.args.ToIPLD()
|
||||
if err != nil {
|
||||
outerErr = err
|
||||
return
|
||||
}
|
||||
})
|
||||
return outerErr
|
||||
}
|
||||
|
||||
func (hc *HttpBearer) verifyHash() error {
|
||||
n, err := hc.originalArgs.GetNode(HttpArgsKey)
|
||||
if err != nil {
|
||||
// no hash found, nothing to verify
|
||||
return nil
|
||||
}
|
||||
|
||||
mhBytes, err := n.AsBytes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("http args hash should be a string")
|
||||
}
|
||||
|
||||
data, err := ipld.Encode(hc.argsIpld, dagcbor.Encode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't encode derived args in dag-cbor: %w", err)
|
||||
}
|
||||
|
||||
sum, err := multihash.Sum(data, multihash.SHA2_256, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(mhBytes, sum) {
|
||||
return fmt.Errorf("derived args from http request don't match the expected hash")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeHttpHash compute the hash of the derived arguments from the HTTP request.
|
||||
// If that hash is inserted at the HttpArgsKey key in the invocation arguments,
|
||||
// this increases the security as the UCAN token cannot be used with a different
|
||||
// HTTP request.
|
||||
func MakeHttpHash(req *http.Request) ([]byte, error) {
|
||||
computedArgs, err := makeHttpArgs(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, err := computedArgs.ToIPLD()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ipld.Encode(n, dagcbor.Encode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sum, err := multihash.Sum(data, multihash.SHA2_256, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func makeHttpArgs(req *http.Request) (*args.Args, error) {
|
||||
n, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "scheme", qp.String(req.URL.Scheme)) // https
|
||||
qp.MapEntry(ma, "method", qp.String(req.Method)) // GET
|
||||
qp.MapEntry(ma, "host", qp.String(req.Host)) // example.com
|
||||
qp.MapEntry(ma, "path", qp.String(req.URL.Path)) // /foo
|
||||
|
||||
qp.MapEntry(ma, "headers", qp.Map(2, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "Origin", qp.String(req.Header.Get("Origin")))
|
||||
qp.MapEntry(ma, "User-Agent", qp.String(req.Header.Get("User-Agent")))
|
||||
}))
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := args.New()
|
||||
err = res.Add(HttpArgsKey, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
185
toolkit/server/bearer/http_test.go
Normal file
185
toolkit/server/bearer/http_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package bearer
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/multiformats/go-multihash"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
func TestHttp(t *testing.T) {
|
||||
pol := policy.MustConstruct(
|
||||
policy.Equal(".http.scheme", literal.String("http")),
|
||||
policy.Equal(".http.method", literal.String("GET")),
|
||||
policy.Equal(".http.host", literal.String("example.com")),
|
||||
policy.Equal(".http.path", literal.String("/foo")),
|
||||
policy.Like(".http.headers.User-Agent", "*Mozilla*"),
|
||||
policy.Equal(".http.headers.Origin", literal.String("dapps.com")),
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
target string
|
||||
headers map[string]string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
method: http.MethodGet,
|
||||
target: "http://example.com/foo",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0",
|
||||
"Origin": "dapps.com",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wrong scheme",
|
||||
method: http.MethodGet,
|
||||
target: "https://example.com/foo",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0",
|
||||
"Origin": "dapps.com",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong method",
|
||||
method: http.MethodPost,
|
||||
target: "http://example.com/foo",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0",
|
||||
"Origin": "dapps.com",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong host",
|
||||
method: http.MethodGet,
|
||||
target: "http://foo.com/foo",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0",
|
||||
"Origin": "dapps.com",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong path",
|
||||
method: http.MethodGet,
|
||||
target: "http://example.com/bar",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0",
|
||||
"Origin": "dapps.com",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong origin",
|
||||
method: http.MethodGet,
|
||||
target: "http://example.com/foo",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0",
|
||||
"Origin": "foo.com",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong user-agent",
|
||||
method: http.MethodGet,
|
||||
target: "http://example.com/foo",
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Chrome/51.0.2704.103 Safari/537.36",
|
||||
"Origin": "dapps.com",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
// we don't test the args hash here
|
||||
emptyArgs := args.New().ReadOnly()
|
||||
|
||||
ctx := NewHttpBearer(pol, emptyArgs, r)
|
||||
|
||||
_, err := ctx.Args()
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.expected {
|
||||
require.NoError(t, ctx.Verify())
|
||||
} else {
|
||||
require.Error(t, ctx.Verify())
|
||||
}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(tc.method, tc.target, nil)
|
||||
for k, v := range tc.headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpHash(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com/foo", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("User-Agent", "Chrome/51.0.2704.103 Safari/537.36")
|
||||
req.Header.Add("Origin", "dapps.com")
|
||||
|
||||
pol := policy.MustConstruct(
|
||||
policy.Equal(".http.scheme", literal.String("http")),
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hash []byte
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "correct hash",
|
||||
hash: must(MakeHttpHash(req)),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-matching hash",
|
||||
hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.SHA2_256, -1)),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong type of hash",
|
||||
hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.BLAKE3, -1)),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no hash",
|
||||
hash: nil,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
invArgs := args.New()
|
||||
err := invArgs.Add(HttpArgsKey, tc.hash)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := NewHttpBearer(pol, invArgs.ReadOnly(), req)
|
||||
|
||||
if tc.expected {
|
||||
require.NoError(t, ctx.Verify())
|
||||
} else {
|
||||
require.Error(t, ctx.Verify())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
168
toolkit/server/bearer/jsonrpc.go
Normal file
168
toolkit/server/bearer/jsonrpc.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package bearer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/INFURA/go-ethlibs/jsonrpc"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/multiformats/go-multihash"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
// JsonRpcArgsKey is the key in the args, used for:
|
||||
// - if it exists in the invocation, holds a hash of the args derived from the JsonRpc request
|
||||
// - in the final args to be evaluated against the policies, holds the args derived from the JsonRpc request
|
||||
const JsonRpcArgsKey = "jsonrpc"
|
||||
|
||||
type JsonRpcBearer struct {
|
||||
pol policy.Policy
|
||||
originalArgs args.ReadOnly
|
||||
req *jsonrpc.Request
|
||||
|
||||
once sync.Once
|
||||
args *args.Args
|
||||
argsIpld ipld.Node
|
||||
}
|
||||
|
||||
func NewJsonRpcBearer(pol policy.Policy, originalArgs args.ReadOnly, req *jsonrpc.Request) *JsonRpcBearer {
|
||||
return &JsonRpcBearer{pol: pol, originalArgs: originalArgs, req: req}
|
||||
}
|
||||
|
||||
func (jrc *JsonRpcBearer) Verify() error {
|
||||
if err := jrc.makeArgs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jrc.verifyHash(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ok, leaf := jrc.pol.PartialMatch(jrc.argsIpld)
|
||||
if !ok {
|
||||
return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (jrc *JsonRpcBearer) Args() (*args.Args, error) {
|
||||
if err := jrc.makeArgs(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jrc.args, nil
|
||||
}
|
||||
|
||||
func (jrc *JsonRpcBearer) makeArgs() error {
|
||||
var outerErr error
|
||||
jrc.once.Do(func() {
|
||||
var err error
|
||||
jrc.args, err = makeJsonRpcArgs(jrc.req)
|
||||
if err != nil {
|
||||
outerErr = err
|
||||
return
|
||||
}
|
||||
|
||||
jrc.argsIpld, err = jrc.args.ToIPLD()
|
||||
if err != nil {
|
||||
outerErr = err
|
||||
return
|
||||
}
|
||||
})
|
||||
return outerErr
|
||||
}
|
||||
|
||||
func (jrc *JsonRpcBearer) verifyHash() error {
|
||||
n, err := jrc.originalArgs.GetNode(JsonRpcArgsKey)
|
||||
if err != nil {
|
||||
// no hash found, nothing to verify
|
||||
return nil
|
||||
}
|
||||
|
||||
mhBytes, err := n.AsBytes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("jsonrpc args hash should be a string")
|
||||
}
|
||||
|
||||
data, err := ipld.Encode(jrc.argsIpld, dagcbor.Encode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't encode derived args in dag-cbor: %w", err)
|
||||
}
|
||||
|
||||
sum, err := multihash.Sum(data, multihash.SHA2_256, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(mhBytes, sum) {
|
||||
return fmt.Errorf("derived args from jsonrpc request don't match the expected hash")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeJsonRpcHash compute the hash of the derived arguments from the JsonRPC request.
|
||||
// If that hash is inserted at the JsonRpcArgsKey key in the invocation arguments,
|
||||
// this increases the security as the UCAN token cannot be used with a different
|
||||
// JsonRPC request.
|
||||
func MakeJsonRpcHash(req *jsonrpc.Request) ([]byte, error) {
|
||||
computedArgs, err := makeJsonRpcArgs(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, err := computedArgs.ToIPLD()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ipld.Encode(n, dagcbor.Encode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sum, err := multihash.Sum(data, multihash.SHA2_256, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func makeJsonRpcArgs(req *jsonrpc.Request) (*args.Args, error) {
|
||||
deserialized := make([]any, len(req.Params))
|
||||
for i, param := range req.Params {
|
||||
err := json.Unmarshal(param, &deserialized[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
params, err := literal.List(deserialized)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, err := qp.BuildMap(basicnode.Prototype.Any, 3, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "jsonrpc", qp.String(req.JSONRPC))
|
||||
qp.MapEntry(ma, "method", qp.String(req.Method))
|
||||
qp.MapEntry(ma, "params", qp.Node(params))
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := args.New()
|
||||
err = res.Add(JsonRpcArgsKey, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
171
toolkit/server/bearer/jsonrpc_test.go
Normal file
171
toolkit/server/bearer/jsonrpc_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package bearer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/INFURA/go-ethlibs/jsonrpc"
|
||||
"github.com/multiformats/go-multihash"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
func TestJsonRpc(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *jsonrpc.Request
|
||||
pol policy.Policy
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "or on method, not matching",
|
||||
req: jsonrpc.MustRequest(1839673506133526, "eth_getBlockByNumber",
|
||||
"0x599784", true,
|
||||
),
|
||||
pol: policy.MustConstruct(
|
||||
policy.Or(
|
||||
policy.Equal(".jsonrpc.method", literal.String("eth_getCode")),
|
||||
policy.Equal(".jsonrpc.method", literal.String("eth_getBalance")),
|
||||
policy.Equal(".jsonrpc.method", literal.String("eth_call")),
|
||||
policy.Equal(".jsonrpc.method", literal.String("eth_blockNumber")),
|
||||
),
|
||||
),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "or on method, matching",
|
||||
req: jsonrpc.MustRequest(1839673506133526, "eth_call",
|
||||
map[string]string{"to": "0xBADBADBADBADBADBADBADBADBADBADBADBADBAD1"},
|
||||
),
|
||||
pol: policy.MustConstruct(
|
||||
policy.Or(
|
||||
policy.Equal(".jsonrpc.method", literal.String("eth_getCode")),
|
||||
policy.Equal(".jsonrpc.method", literal.String("eth_getBalance")),
|
||||
policy.Equal(".jsonrpc.method", literal.String("eth_call")),
|
||||
policy.Equal(".jsonrpc.method", literal.String("eth_blockNumber")),
|
||||
),
|
||||
),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "complex, optional parameter, matching",
|
||||
req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall",
|
||||
true, false, 1234, "callTracer",
|
||||
),
|
||||
pol: policy.MustConstruct(
|
||||
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
|
||||
policy.Or(
|
||||
policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")),
|
||||
policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")),
|
||||
),
|
||||
),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "complex, optional parameter, missing parameter",
|
||||
req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall",
|
||||
true, false, 1234,
|
||||
),
|
||||
pol: policy.MustConstruct(
|
||||
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
|
||||
policy.Or(
|
||||
policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")),
|
||||
policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")),
|
||||
),
|
||||
),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "complex, parameter not matching",
|
||||
req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall",
|
||||
true, false, 1234, "ho_no",
|
||||
),
|
||||
pol: policy.MustConstruct(
|
||||
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
|
||||
policy.Or(
|
||||
policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")),
|
||||
policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")),
|
||||
),
|
||||
),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// we don't test the args hash here
|
||||
emptyArgs := args.New().ReadOnly()
|
||||
|
||||
ctx := NewJsonRpcBearer(tc.pol, emptyArgs, tc.req)
|
||||
|
||||
_, err := ctx.Args()
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.expected {
|
||||
require.NoError(t, ctx.Verify())
|
||||
} else {
|
||||
require.Error(t, ctx.Verify())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonRpcHash(t *testing.T) {
|
||||
req := jsonrpc.MustRequest(1839673506133526, "debug_traceCall",
|
||||
true, false, 1234, "ho_no",
|
||||
)
|
||||
pol := policy.MustConstruct(
|
||||
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hash []byte
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "correct hash",
|
||||
hash: must(MakeJsonRpcHash(req)),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-matching hash",
|
||||
hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.SHA2_256, -1)),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong type of hash",
|
||||
hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.BLAKE3, -1)),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no hash",
|
||||
hash: nil,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
invArgs := args.New()
|
||||
err := invArgs.Add(JsonRpcArgsKey, tc.hash)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := NewJsonRpcBearer(pol, invArgs.ReadOnly(), req)
|
||||
|
||||
if tc.expected {
|
||||
require.NoError(t, ctx.Verify())
|
||||
} else {
|
||||
require.Error(t, ctx.Verify())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func must[T any](t T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package server
|
||||
|
||||
import "context"
|
||||
|
||||
type ctxUserIdKey struct{}
|
||||
|
||||
// ContextGetUserId return the UserId stored in the context, if it exists.
|
||||
func ContextGetUserId(ctx context.Context) (string, bool) {
|
||||
val, ok := ctx.Value(ctxUserIdKey{}).(string)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func addUserIdToContext(ctx context.Context, userId string) context.Context {
|
||||
return context.WithValue(ctx, ctxUserIdKey{}, userId)
|
||||
}
|
||||
|
||||
type ctxProjectIdKey struct{}
|
||||
|
||||
// ContextGetProjectId return the ProjectID stored in the context, if it exists.
|
||||
func ContextGetProjectId(ctx context.Context) (string, bool) {
|
||||
val, ok := ctx.Value(ctxProjectIdKey{}).(string)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func addProjectIdToContext(ctx context.Context, projectId string) context.Context {
|
||||
return context.WithValue(ctx, ctxProjectIdKey{}, projectId)
|
||||
}
|
||||
16
toolkit/server/exectx/ctxvalue.go
Normal file
16
toolkit/server/exectx/ctxvalue.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package exectx
|
||||
|
||||
import "context"
|
||||
|
||||
type ctxKey struct{}
|
||||
|
||||
// AddUcanCtxToContext insert a UcanCtx into a go context.
|
||||
func AddUcanCtxToContext(ctx context.Context, ucanCtx *UcanCtx) context.Context {
|
||||
return context.WithValue(ctx, ctxKey{}, ucanCtx)
|
||||
}
|
||||
|
||||
// FromContext retrieve a UcanCtx from a go context.
|
||||
func FromContext(ctx context.Context) (*UcanCtx, bool) {
|
||||
ucanCtx, ok := ctx.Value(ctxKey{}).(*UcanCtx)
|
||||
return ucanCtx, ok
|
||||
}
|
||||
146
toolkit/server/exectx/ucanctx.go
Normal file
146
toolkit/server/exectx/ucanctx.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package exectx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/INFURA/go-ethlibs/jsonrpc"
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/container"
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||
|
||||
bearer2 "github.com/INFURA/go-ucan-toolkit/server/bearer"
|
||||
)
|
||||
|
||||
var _ delegation.Loader = UcanCtx{}
|
||||
|
||||
// UcanCtx is a UCAN execution context meant to be inserted in the go context while handling a request.
|
||||
// It allows to handle the control of the execution in multiple steps across different middlewares,
|
||||
// as well as doing "bearer" types of controls, when arguments are derived from the request itself (HTTP, JsonRpc).
|
||||
type UcanCtx struct {
|
||||
inv *invocation.Token
|
||||
dlgs map[cid.Cid]*delegation.Token
|
||||
|
||||
policies policy.Policy // all policies combined
|
||||
meta *meta.Meta // all meta combined, with no overwriting
|
||||
|
||||
// argument sources
|
||||
http *bearer2.HttpBearer
|
||||
jsonrpc *bearer2.JsonRpcBearer
|
||||
}
|
||||
|
||||
func FromContainer(cont container.Reader) (*UcanCtx, error) {
|
||||
inv, err := cont.GetInvocation()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no invocation: %w", err)
|
||||
}
|
||||
|
||||
ctx := &UcanCtx{
|
||||
inv: inv,
|
||||
dlgs: make(map[cid.Cid]*delegation.Token, len(cont)-1),
|
||||
meta: meta.NewMeta(),
|
||||
}
|
||||
|
||||
// iterate in reverse, from the root delegation to the leaf
|
||||
for _, c := range slices.Backward(inv.Proof()) {
|
||||
// make sure we have the delegation
|
||||
dlg, err := cont.GetDelegation(c)
|
||||
if errors.Is(err, delegation.ErrDelegationNotFound) {
|
||||
return nil, fmt.Errorf("delegation proof %s is missing", c)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.dlgs[c] = dlg
|
||||
// accumulate the policies
|
||||
ctx.policies = append(ctx.policies, dlg.Policy()...)
|
||||
// accumulate the meta values, with no overwriting
|
||||
ctx.meta.Include(dlg.Meta())
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// Command returns the command triggered by the invocation.
|
||||
func (ctn UcanCtx) Command() command.Command {
|
||||
return ctn.inv.Command()
|
||||
}
|
||||
|
||||
// Invocation returns the invocation.Token.
|
||||
func (ctn UcanCtx) Invocation() *invocation.Token {
|
||||
return ctn.inv
|
||||
}
|
||||
|
||||
// GetDelegation returns the delegation.Token matching the given CID.
|
||||
// If not found, delegation.ErrDelegationNotFound is returned.
|
||||
func (ctn UcanCtx) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
||||
if dlg, ok := ctn.dlgs[cid]; ok {
|
||||
return dlg, nil
|
||||
}
|
||||
return nil, delegation.ErrDelegationNotFound
|
||||
}
|
||||
|
||||
// Policies return the full set of policy statements to satisfy.
|
||||
func (ctn UcanCtx) Policies() policy.Policy {
|
||||
return ctn.policies
|
||||
}
|
||||
|
||||
// Meta returns all the meta values from the delegations.
|
||||
// They are accumulated from the root delegation to the leaf delegation, with no overwrite.
|
||||
func (ctn UcanCtx) Meta() meta.ReadOnly {
|
||||
return ctn.meta.ReadOnly()
|
||||
}
|
||||
|
||||
// VerifyHttp verify the delegation's policies against arguments constructed from the HTTP request.
|
||||
// This function can only be called once per context.
|
||||
// After being used, those constructed arguments will be used in ExecutionAllowed as well.
|
||||
func (ctn UcanCtx) VerifyHttp(req *http.Request) error {
|
||||
if ctn.http == nil {
|
||||
panic("only use once per request context")
|
||||
}
|
||||
ctn.http = bearer2.NewHttpBearer(ctn.policies, ctn.inv.Arguments(), req)
|
||||
return ctn.http.Verify()
|
||||
}
|
||||
|
||||
// VerifyJsonRpc verify the delegation's policies against arguments constructed from the JsonRpc request.
|
||||
// This function can only be called once per context.
|
||||
// After being used, those constructed arguments will be used in ExecutionAllowed as well.
|
||||
func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error {
|
||||
if ctn.jsonrpc != nil {
|
||||
panic("only use once per request context")
|
||||
}
|
||||
ctn.jsonrpc = bearer2.NewJsonRpcBearer(ctn.policies, ctn.inv.Arguments(), req)
|
||||
return ctn.jsonrpc.Verify()
|
||||
}
|
||||
|
||||
// ExecutionAllowed does the final verification of the invocation.
|
||||
// If VerifyHttp or VerifyJsonRpc has been used, those arguments are part of the verification.
|
||||
func (ctn UcanCtx) ExecutionAllowed() error {
|
||||
return ctn.inv.ExecutionAllowedWithArgsHook(ctn, func(args args.ReadOnly) (*args.Args, error) {
|
||||
newArgs := args.WriteableClone()
|
||||
|
||||
if ctn.http != nil {
|
||||
httpArgs, err := ctn.http.Args()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArgs.Include(httpArgs)
|
||||
}
|
||||
if ctn.jsonrpc != nil {
|
||||
jsonRpcArgs, err := ctn.jsonrpc.Args()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArgs.Include(jsonRpcArgs)
|
||||
}
|
||||
|
||||
return newArgs, nil
|
||||
})
|
||||
}
|
||||
162
toolkit/server/exectx/ucanctx_test.go
Normal file
162
toolkit/server/exectx/ucanctx_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package exectx_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/INFURA/go-ethlibs/jsonrpc"
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/container"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||
|
||||
exectx2 "github.com/INFURA/go-ucan-toolkit/server/exectx"
|
||||
)
|
||||
|
||||
func ExampleContext() {
|
||||
// let's use some pre-made DID+privkey.
|
||||
// use go-ucan/did to generate or parse them.
|
||||
service := didtest.PersonaAlice
|
||||
user := didtest.PersonaBob
|
||||
|
||||
// DELEGATION: the service gives some power to the user, in the form of a root UCAN token.
|
||||
// The command defines the shape of the arguments on which the policies operate, it is specific to that service.
|
||||
// Policies define what the user can do.
|
||||
|
||||
cmd := command.New("foo")
|
||||
pol := policy.MustConstruct(
|
||||
// some basic HTTP constraints
|
||||
policy.Equal(".http.method", literal.String("GET")),
|
||||
policy.Like(".http.path", "/foo/*"),
|
||||
// some JsonRpc constraints
|
||||
policy.Or(
|
||||
policy.Like(".jsonrpc.method", "eth_*"),
|
||||
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
|
||||
),
|
||||
)
|
||||
|
||||
dlg, _ := delegation.Root(service.DID(), user.DID(), cmd, pol,
|
||||
delegation.WithExpirationIn(24*time.Hour),
|
||||
)
|
||||
dlgBytes, dlgCid, _ := dlg.ToSealed(service.PrivKey())
|
||||
|
||||
// INVOCATION: the user leverages the delegation (power) to make a request.
|
||||
|
||||
inv, _ := invocation.New(user.DID(), service.DID(), cmd, []cid.Cid{dlgCid},
|
||||
invocation.WithExpirationIn(10*time.Minute),
|
||||
invocation.WithArgument("myarg", "hello"), // we can specify invocation parameters
|
||||
)
|
||||
invBytes, invCid, _ := inv.ToSealed(user.PrivKey())
|
||||
|
||||
// PACKAGING: no obligation for the transport, but the user needs to give the service the invocation
|
||||
// and all the proof delegations. We can use a container for that.
|
||||
cont := container.NewWriter()
|
||||
cont.AddSealed(dlgCid, dlgBytes)
|
||||
cont.AddSealed(invCid, invBytes)
|
||||
contBytes, _ := cont.ToCborBase64()
|
||||
|
||||
// MAKING A REQUEST: we pass the container in the Bearer HTTP header
|
||||
|
||||
jrpc := jsonrpc.NewRequest()
|
||||
jrpc.Method = "eth_call"
|
||||
jrpc.Params = jsonrpc.MustParams("0x599784", true)
|
||||
jrpcBytes, _ := jrpc.MarshalJSON()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/foo/bar", bytes.NewReader(jrpcBytes))
|
||||
req.Header.Set("Authorization", "Bearer "+string(contBytes))
|
||||
|
||||
// SERVER: Auth middleware
|
||||
// - decode the container
|
||||
// - create the context
|
||||
|
||||
authMw := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Note: we obviously want something more robust, this is an example
|
||||
// Note: if an error occur, we'll want to return an HTTP 401 Unauthorized
|
||||
data := bytes.TrimPrefix([]byte(r.Header.Get("Authorization")), []byte("Bearer "))
|
||||
cont, _ := container.FromCborBase64(data)
|
||||
ucanCtx, _ := exectx2.FromContainer(cont)
|
||||
|
||||
// insert into the go context
|
||||
req = req.WithContext(exectx2.AddUcanCtxToContext(req.Context(), ucanCtx))
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// SERVER: http checks
|
||||
|
||||
httpMw := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ucanCtx, _ := exectx2.FromContext(req.Context())
|
||||
|
||||
err := ucanCtx.VerifyHttp(r)
|
||||
if err != nil {
|
||||
// This will print something like:
|
||||
// `the following UCAN policy is not satisfied: ["==", ".http.path", "/foo"]`
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// SERVER: JsonRpc checks
|
||||
|
||||
jsonrpcMw := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ucanCtx, _ := exectx2.FromContext(req.Context())
|
||||
|
||||
var jrpc jsonrpc.Request
|
||||
_ = json.NewDecoder(r.Body).Decode(&jrpc)
|
||||
|
||||
err := ucanCtx.VerifyJsonRpc(&jrpc)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// SERVER: final handler
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
ucanCtx, _ := exectx2.FromContext(req.Context())
|
||||
|
||||
if err := ucanCtx.ExecutionAllowed(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(w, "Success!")
|
||||
}
|
||||
|
||||
// Ready to go!
|
||||
_ = http.ListenAndServe("", authMw(httpMw(jsonrpcMw(http.HandlerFunc(handler)))))
|
||||
}
|
||||
|
||||
func TestGoCtx(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ucanCtx, ok := exectx2.FromContext(ctx)
|
||||
require.False(t, ok)
|
||||
require.Nil(t, ucanCtx)
|
||||
|
||||
expected := &exectx2.UcanCtx{}
|
||||
|
||||
ctx = exectx2.AddUcanCtxToContext(ctx, expected)
|
||||
|
||||
got, ok := exectx2.FromContext(ctx)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expected, got)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
)
|
||||
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
|
||||
func ExampleMiddleware(serviceDID did.DID) Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
tokenReader, ok := extractBearerToken(r)
|
||||
if !ok {
|
||||
http.Error(w, "missing or malformed UCAN HTTP Bearer token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// decode
|
||||
// TODO: ultimately, this token should be a container with one invocation and 1+ delegations.
|
||||
// We are doing something simpler for now.
|
||||
dlg, err := delegation.FromDagCborReader(tokenReader)
|
||||
if err != nil {
|
||||
http.Error(w, "malformed UCAN delegation", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// optional: http-bearer
|
||||
|
||||
// validate
|
||||
if dlg.Subject() != serviceDID {
|
||||
http.Error(w, "invalid UCAN delegation", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// TODO: policies check + inject in context
|
||||
|
||||
// extract values
|
||||
userId, err := dlg.Meta().GetString("userId")
|
||||
if err != nil {
|
||||
http.Error(w, "missing or malformed userId", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
projectId, err := dlg.Meta().GetString("projectId")
|
||||
if err != nil {
|
||||
http.Error(w, "missing or malformed projectId", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// inject into context
|
||||
ctx = addUserIdToContext(ctx, userId)
|
||||
ctx = addProjectIdToContext(ctx, projectId)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func extractBearerToken(r *http.Request) (io.Reader, bool) {
|
||||
header := r.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(header, "Bearer ") {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// skip prefix
|
||||
reader := strings.NewReader(header[len("Bearer "):])
|
||||
|
||||
// base64 decode
|
||||
return base64.NewDecoder(base64.StdEncoding, reader), true
|
||||
}
|
||||
Reference in New Issue
Block a user