Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/base skeleton #1

Merged
merged 11 commits into from
Aug 9, 2024
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
target/
.idea/
.vscode/
.DS_Store
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Template project
# axone-sdk

> Template for [Axone](https://axone.xyz) opensource projects.

[![lint](https://img.shields.io/github/actions/workflow/status/axone-protocol/template-oss/lint.yml?branch=main&label=lint&style=for-the-badge&logo=github)](https://github.com/axone-protocol/template-oss/actions/workflows/lint.yml)
[![lint](https://img.shields.io/github/actions/workflow/status/axone-protocol/axone-sdk/lint.yml?branch=main&label=lint&style=for-the-badge&logo=github)](https://github.com/axone-protocol/axone-sdk/actions/workflows/lint.yml)
[![conventional commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=for-the-badge&logo=conventionalcommits)](https://conventionalcommits.org)
[![contributor covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=for-the-badge)](https://github.com/axone-protocol/.github/blob/main/CODE_OF_CONDUCT.md)
[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg?style=for-the-badge)](https://opensource.org/licenses/BSD-3-Clause)
Expand Down
5 changes: 5 additions & 0 deletions auth/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package auth

import "net/http"

type AuthenticatedHandler func(*Identity, http.ResponseWriter, *http.Request)
16 changes: 16 additions & 0 deletions auth/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package auth

// Identity denotes an identity that has been authenticated, which may contain some resolved authorizations.
type Identity struct {
DID string
AuthorizedActions []string
}

func (i Identity) Can(action string) bool {
for _, a := range i.AuthorizedActions {
if a == action {
return true
}
}
return false
}
12 changes: 12 additions & 0 deletions auth/jwt/claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package jwt

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

type ProxyClaims struct {
jwt.StandardClaims
Can Permissions `json:"can"`
}

type Permissions struct {
Actions []string `json:"actions"`
}
56 changes: 56 additions & 0 deletions auth/jwt/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package jwt

import (
"context"
"errors"
"io"
"net/http"

"github.com/axone-protocol/axone-sdk/auth"
)

func (f *Factory) 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 {
// ...
}

id, err := proxy.Authenticate(context.Background(), credential)
if err != nil {
// ...
}

token, err := f.IssueJWT(id)
if err != nil {
// ...
}

writer.Header().Set("Content-Type", "application/json")
if _, err := writer.Write([]byte(token)); err != nil {
// ...
}
})
}

func (f *Factory) VerifyHTTPMiddleware(next auth.AuthenticatedHandler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
id, err := f.VerifyHTTPRequest(request)
if err != nil {
writer.WriteHeader(http.StatusUnauthorized)
writer.Write([]byte(err.Error()))
return
}

next(id, writer, request)
})
}

func (f *Factory) 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:])
}
66 changes: 66 additions & 0 deletions auth/jwt/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package jwt

import (
"fmt"
"time"

"github.com/axone-protocol/axone-sdk/auth"

"github.com/golang-jwt/jwt"
"github.com/google/uuid"
)

type Factory struct {
secretKey []byte
issuer string
ttl time.Duration
}

func NewFactory(secretKey []byte, issuer string, ttl time.Duration) *Factory {
return &Factory{
secretKey: secretKey,
issuer: issuer,
ttl: ttl,
}
}

func (f *Factory) 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(),
Id: uuid.New().String(),
IssuedAt: now.Unix(),
Issuer: f.issuer,
NotBefore: now.Unix(),
Subject: identity.DID,
},
Can: Permissions{
Actions: identity.AuthorizedActions,
},
}).SignedString(f.secretKey)
}

func (f *Factory) VerifyJWT(raw string) (*auth.Identity, error) {
token, err := jwt.ParseWithClaims(raw, &ProxyClaims{}, func(_ *jwt.Token) (interface{}, error) {
return f.secretKey, nil
})
if err != nil {
return nil, err
}

if !token.Valid {
return nil, fmt.Errorf("invalid token")
}

claims, ok := token.Claims.(*ProxyClaims)
if !ok {
return nil, fmt.Errorf("invalid claims")
}

return &auth.Identity{
DID: claims.Subject,
AuthorizedActions: claims.Can.Actions,
}, nil
}
48 changes: 48 additions & 0 deletions auth/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package auth

import (
"context"
"fmt"

"github.com/axone-protocol/axone-sdk/dataverse"
)

// Proxy acts as the entrypoint of a service and is responsible for authenticating any identity willing to conduct some
// actions against the underlying service.
// It authenticates Decentralized Identities based on a provided Verifiable Credential and resolving allowed authorized
// actions for this identity based on on-chain rules.
// It is not responsible or aware of the communication protocol, which means it only returns information on the identity
// if authentic and won't for example issue a JWT token, this is out of its scope.
type Proxy interface {
// Authenticate verifies the authenticity and integrity of the provided credential before resolving on-chain
// authorized actions with the proxied service by querying the service's governance.
Authenticate(ctx context.Context, credential []byte) (*Identity, error)
}

type authProxy struct {
dvClient dataverse.Client
govAddr string
}

func NewProxy(govAddr string, dvClient dataverse.Client) Proxy {
return &authProxy{
dvClient: dvClient,
govAddr: govAddr,
}
}

func (a *authProxy) Authenticate(ctx context.Context, credential []byte) (*Identity, error) {
// parse credential
// verify signature
// get authorized actions from governance, ex:
did := "did:key:example"
res, err := a.dvClient.ExecGov(ctx, a.govAddr, fmt.Sprintf("can(Action,'%s').", did))
if err != nil {
return nil, err
}

return &Identity{
DID: did,
AuthorizedActions: res.([]string),
}, nil
}
8 changes: 8 additions & 0 deletions dataverse/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dataverse

import "context"

type Client interface {
GetGovAddr(context.Context, string) (string, error)
ExecGov(context.Context, string, string) (interface{}, error)
}
Loading
Loading