Skip to content

Commit

Permalink
Merge pull request #39 from axone-protocol/feat/sanitize-api
Browse files Browse the repository at this point in the history
  • Loading branch information
amimart authored Oct 14, 2024
2 parents e0ad952 + da76a81 commit 44fbac2
Show file tree
Hide file tree
Showing 21 changed files with 143 additions and 42 deletions.
2 changes: 2 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package auth aims to provide authentication primitives against applications and services of the Axone network.
package auth
8 changes: 8 additions & 0 deletions auth/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@ package auth

import "net/http"

// AuthenticatedHandler is a handler that requires an authenticated user.
//
// It is intended to be used in combination with a middleware handler that verifies the user's identity, for example
// with the jwt middleware:
//
// jwtFactory.VerifyHTTPMiddleware(func(id *auth.Identity, w http.ResponseWriter, r *http.Request) {
// // Your handler logic here
// })
type AuthenticatedHandler func(*Identity, http.ResponseWriter, *http.Request)
1 change: 1 addition & 0 deletions auth/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Identity struct {
AuthorizedActions []string
}

// Can check if the identity is authorized to perform a specific action.
func (i Identity) Can(action string) bool {
for _, a := range i.AuthorizedActions {
if a == action {
Expand Down
2 changes: 2 additions & 0 deletions auth/jwt/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package jwt

import "github.com/golang-jwt/jwt"

// ProxyClaims is the set of claims that are included in the JWT token.
type ProxyClaims struct {
jwt.StandardClaims
Can Permissions `json:"can"`
}

// Permissions is the set of permissions that are included in the JWT token.
type Permissions struct {
Actions []string `json:"actions"`
}
17 changes: 11 additions & 6 deletions auth/jwt/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (
"github.com/axone-protocol/axone-sdk/auth"
)

func (f *Factory) HTTPAuthHandler(proxy auth.Proxy) http.Handler {
// HTTPAuthHandler returns an HTTP handler that authenticates an auth.Identity and issues a related JWT token.
func (issuer *Issuer) HTTPAuthHandler(proxy auth.Proxy) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
credential, err := io.ReadAll(request.Body)
if err != nil {
Expand All @@ -24,7 +25,7 @@ func (f *Factory) HTTPAuthHandler(proxy auth.Proxy) http.Handler {
return
}

token, err := f.IssueJWT(id)
token, err := issuer.IssueJWT(id)
if err != nil {
http.Error(writer, fmt.Errorf("failed to issue JWT: %w", err).Error(), http.StatusInternalServerError)
return
Expand All @@ -38,9 +39,11 @@ func (f *Factory) HTTPAuthHandler(proxy auth.Proxy) http.Handler {
})
}

func (f *Factory) VerifyHTTPMiddleware(next auth.AuthenticatedHandler) http.Handler {
// VerifyHTTPMiddleware returns an HTTP middleware that verifies the authenticity of a JWT token before forwarding the
// request to the next auth.AuthenticatedHandler providing the resolve auth.Identity.
func (issuer *Issuer) VerifyHTTPMiddleware(next auth.AuthenticatedHandler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
id, err := f.VerifyHTTPRequest(request)
id, err := issuer.VerifyHTTPRequest(request)
if err != nil {
http.Error(writer, err.Error(), http.StatusUnauthorized)
return
Expand All @@ -50,11 +53,13 @@ func (f *Factory) VerifyHTTPMiddleware(next auth.AuthenticatedHandler) http.Hand
})
}

func (f *Factory) VerifyHTTPRequest(r *http.Request) (*auth.Identity, error) {
// VerifyHTTPRequest checks the authenticity of the JWT token from the given HTTP request and returns the authenticated
// auth.Identity.
func (issuer *Issuer) VerifyHTTPRequest(r *http.Request) (*auth.Identity, error) {
authHeader := r.Header.Get("Authorization")
if len(authHeader) < 7 || authHeader[:6] != "Bearer" {
return nil, errors.New("couldn't find bearer token")
}

return f.VerifyJWT(authHeader[7:])
return issuer.VerifyJWT(authHeader[7:])
}
18 changes: 9 additions & 9 deletions auth/jwt/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"go.uber.org/mock/gomock"
)

func TestFactory_HTTPAuthHandler(t *testing.T) {
func TestIssuer_HTTPAuthHandler(t *testing.T) {
tests := []struct {
name string
body []byte
Expand Down Expand Up @@ -45,14 +45,14 @@ func TestFactory_HTTPAuthHandler(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Convey("Given a JWT factory and mocked auth proxy on mocked http server", t, func() {
factory := jwt.NewFactory(nil, "issuer", 5*time.Second)
Convey("Given a JWT issuer and mocked auth proxy on mocked http server", t, func() {
issuer := jwt.NewIssuer(nil, "issuer", 5*time.Second)

controller := gomock.NewController(t)
defer controller.Finish()

mockAuthProxy := testutil.NewMockProxy(controller)
handler := factory.HTTPAuthHandler(mockAuthProxy)
handler := issuer.HTTPAuthHandler(mockAuthProxy)

mockAuthProxy.EXPECT().Authenticate(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, body []byte) (interface{}, error) {
Expand Down Expand Up @@ -80,9 +80,9 @@ func TestFactory_HTTPAuthHandler(t *testing.T) {
}
}

func TestFactory_VerifyHTTPMiddleware(t *testing.T) {
func TestIssuer_VerifyHTTPMiddleware(t *testing.T) {
// Generate a valid token for testing purpose
token, err := jwt.NewFactory([]byte("secret"), "issuer", 5*time.Second).IssueJWT(&auth.Identity{
token, err := jwt.NewIssuer([]byte("secret"), "issuer", 5*time.Second).IssueJWT(&auth.Identity{
DID: "did:key:zQ3shpoUHzwcgdt2gxjqHHnJnNkBVd4uX3ZBhmPiM7J93yqCr",
AuthorizedActions: nil,
})
Expand Down Expand Up @@ -122,8 +122,8 @@ func TestFactory_VerifyHTTPMiddleware(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Convey("Given a JWT factory and mocked authenticated handler", t, func() {
factory := jwt.NewFactory([]byte("secret"), "issuer", 5*time.Second)
Convey("Given a JWT issuer and mocked authenticated handler", t, func() {
issuer := jwt.NewIssuer([]byte("secret"), "issuer", 5*time.Second)

mockHandler := func(id *auth.Identity, w http.ResponseWriter, _ *http.Request) {
So(id, ShouldResemble, test.expectedIdentity)
Expand All @@ -132,7 +132,7 @@ func TestFactory_VerifyHTTPMiddleware(t *testing.T) {
So(err, ShouldBeNil)
}

middleware := factory.VerifyHTTPMiddleware(mockHandler)
middleware := issuer.VerifyHTTPMiddleware(mockHandler)

Convey("When the VerifyHTTPMiddleware is called", func() {
req, err := http.NewRequest("GET", "/", nil) //nolint:noctx
Expand Down
23 changes: 14 additions & 9 deletions auth/jwt/jwt.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package jwt brings a mean to manage JWT tokens on top of Axone network authentication mechanisms.
package jwt

import (
Expand All @@ -9,41 +10,45 @@ import (
"github.com/google/uuid"
)

type Factory struct {
// Issuer is an entity responsible to issue and verify JWT tokens.
type Issuer struct {
secretKey []byte
issuer string
ttl time.Duration
}

func NewFactory(secretKey []byte, issuer string, ttl time.Duration) *Factory {
return &Factory{
// NewIssuer creates a new Issuer with the given secret key, issuer and token time-to-live.
func NewIssuer(secretKey []byte, issuer string, ttl time.Duration) *Issuer {
return &Issuer{
secretKey: secretKey,
issuer: issuer,
ttl: ttl,
}
}

func (f *Factory) IssueJWT(identity *auth.Identity) (string, error) {
// IssueJWT forge and sign a new JWT token for the given authenticated auth.Identity.
func (issuer *Issuer) IssueJWT(identity *auth.Identity) (string, error) {
now := time.Now()
return jwt.NewWithClaims(jwt.SigningMethodHS256, ProxyClaims{
StandardClaims: jwt.StandardClaims{
Audience: identity.DID,
ExpiresAt: now.Add(f.ttl).Unix(),
ExpiresAt: now.Add(issuer.ttl).Unix(),
Id: uuid.New().String(),
IssuedAt: now.Unix(),
Issuer: f.issuer,
Issuer: issuer.issuer,
NotBefore: now.Unix(),
Subject: identity.DID,
},
Can: Permissions{
Actions: identity.AuthorizedActions,
},
}).SignedString(f.secretKey)
}).SignedString(issuer.secretKey)
}

func (f *Factory) VerifyJWT(raw string) (*auth.Identity, error) {
// VerifyJWT checks the validity and the signature of the given JWT token and returns the authenticated identity.
func (issuer *Issuer) VerifyJWT(raw string) (*auth.Identity, error) {
token, err := jwt.ParseWithClaims(raw, &ProxyClaims{}, func(_ *jwt.Token) (interface{}, error) {
return f.secretKey, nil
return issuer.secretKey, nil
})
if err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions auth/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type authProxy struct {
serviceID string
}

// NewProxy creates a new Proxy instance using the given service identifier and on-chain governance address (i.e. the
// law-stone smart contract instance carrying its rules).
func NewProxy(govAddr, serviceID string,
dvClient dataverse.QueryClient,
authParser credential.Parser[*credential.AuthClaim],
Expand All @@ -39,6 +41,8 @@ func NewProxy(govAddr, serviceID string,
}
}

// Authenticate verifies the authenticity and integrity of the provided credential before resolving on-chain
// authorized actions with the proxied service by querying its governance.
func (a *authProxy) Authenticate(ctx context.Context, credential []byte) (*Identity, error) {
authClaim, err := a.authParser.ParseSigned(credential)
if err != nil {
Expand Down
5 changes: 4 additions & 1 deletion credential/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const ErrAuthClaim MessageError = "invalid auth claim"

var _ Claim = (*AuthClaim)(nil)

// AuthClaim carries the claims of a [verifiable.Credential] for authentication purpose.
type AuthClaim struct {
ID string
ToService string
Expand Down Expand Up @@ -43,18 +44,20 @@ func (ac *AuthClaim) From(vc *verifiable.Credential) error {

var _ Parser[*AuthClaim] = (*AuthParser)(nil)

// AuthParser is a [verifiable.Credential] parser expected to carry [AuthClaim].
type AuthParser struct {
*DefaultParser
}

// NewAuthParser creates a new AuthParser using the provided [ld.DocumentLoader].
func NewAuthParser(documentLoader ld.DocumentLoader) *AuthParser {
return &AuthParser{
DefaultParser: &DefaultParser{documentLoader: documentLoader},
}
}

func (ap *AuthParser) ParseSigned(raw []byte) (*AuthClaim, error) {
cred, err := ap.parseSigned(raw)
cred, err := ap.DefaultParser.ParseSigned(raw)
if err != nil {
return nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions credential/credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Package credential aims to provide a set of tools to work with verifiable credentials. It includes necessary components
// to parse, verify, issue and sign credentials.
//
// Although the components are designed to be used in a modular way, some provided types and intentionally specific to
// the Axone Ontology.
package credential
17 changes: 7 additions & 10 deletions credential/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,14 @@ type Generator struct {
parser *DefaultParser
}

// New allow to Generate a verifiable credential with the given credential descriptor.
// New allows to Generate a verifiable credential with the given credential descriptor.
// Example:
//
// vc, err := credential.New(
// template.NewGovernance(
// "datasetID",
// "addr",
// WithID[*GovernanceDescriptor]("id")
// ),
// WithParser(parser),
// WithSigner(signer)). // Signature is optional and Generate a not signed VC if not provided.
// Generate()
// vc, err := credential.New(
// template.NewGovernance("datasetID", "addr", template.WithID[*GovernanceDescriptor]("id")),
// WithParser(parser),
// WithSigner(signer) // Signature is optional and Generate a not signed VC if not provided.
// ).Generate()
func New(descriptor Descriptor, opts ...Option) *Generator {
g := &Generator{
vc: descriptor,
Expand All @@ -62,6 +58,7 @@ func WithSigner(signer keys.Keyring) Option {
}
}

// Generate generates and sign the [verifiable.Credential].
func (generator *Generator) Generate() (*verifiable.Credential, error) {
raw, err := generator.vc.Generate()
if err != nil {
Expand Down
12 changes: 11 additions & 1 deletion credential/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,31 @@ import (
"github.com/piprate/json-gold/ld"
)

// Claim denotes a claim carried by a [verifiable.Credential].
type Claim interface {
// From extracts the Claim from a [verifiable.Credential].
From(vc *verifiable.Credential) error
}

// Parser is a [verifiable.Credential] parser for a certain type of Claim.
type Parser[T Claim] interface {
// ParseSigned parse and verify the authenticity and integrity of a [verifiable.Credential] before returning its Claim.
ParseSigned(raw []byte) (T, error)
}

// DefaultParser is a simple [verifiable.Credential] parser.
type DefaultParser struct {
documentLoader ld.DocumentLoader
}

// NewDefaultParser creates a new DefaultParser using the provided [ld.DocumentLoader].
func NewDefaultParser(documentLoader ld.DocumentLoader) *DefaultParser {
return &DefaultParser{documentLoader: documentLoader}
}

// Parse parses a [verifiable.Credential] from a raw byte slice.
//
// It does not verify its proof, if you can to check the credential authenticity and integrity use ParseSigned instead.
func (cp *DefaultParser) Parse(raw []byte) (*verifiable.Credential, error) {
vc, err := verifiable.ParseCredential(
raw,
Expand All @@ -42,7 +51,8 @@ func (cp *DefaultParser) Parse(raw []byte) (*verifiable.Credential, error) {
return vc, nil
}

func (cp *DefaultParser) parseSigned(raw []byte) (*verifiable.Credential, error) {
// ParseSigned parse and verify the authenticity and integrity of a [verifiable.Credential].
func (cp *DefaultParser) ParseSigned(raw []byte) (*verifiable.Credential, error) {
vc, err := verifiable.ParseCredential(
raw,
verifiable.WithJSONLDValidation(),
Expand Down
3 changes: 3 additions & 0 deletions credential/template/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package template provides some predefined descriptors used to issue verifiable credentials, they are compliant with the
// Axone Ontology.
package template
3 changes: 3 additions & 0 deletions dataverse/dataverse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package dataverse provides programmatic access to the Axone protocol, it carries the high level logic to interacts with
// its components hiding the low level details.
package dataverse
Loading

0 comments on commit 44fbac2

Please sign in to comment.