Skip to content

Commit

Permalink
rpc: implement getIdToken intent handling
Browse files Browse the repository at this point in the history
  • Loading branch information
patrislav committed Jul 5, 2024
1 parent 09113c7 commit e5712a2
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 1 deletion.
13 changes: 12 additions & 1 deletion rpc/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ QwIDAQAB
VerificationContextsTable: "VerificationContextsTable",
},
KMS: config.KMSConfig{
SigningKey: "arn:aws:kms:us-east-1:000000000000:key/5edb0219-8da9-4842-98fb-e83c6316f3bd",
TenantKeys: []string{"arn:aws:kms:us-east-1:000000000000:key/27ebbde0-49d2-4cb6-ad78-4f2c24fe7b79"},
DefaultSessionKeys: []string{"arn:aws:kms:us-east-1:000000000000:key/27ebbde0-49d2-4cb6-ad78-4f2c24fe7b79"},
},
Expand Down Expand Up @@ -301,6 +302,7 @@ func newAccount(t *testing.T, tnt *data.Tenant, enc *enclave.Enclave, issuer str
wallet, err = ethwallet.NewWalletFromRandomEntropy()
require.NoError(t, err)
}
signingSession := intents.NewSessionP256K1(wallet)

identity := proto.Identity{
Type: proto.IdentityType_OIDC,
Expand All @@ -309,7 +311,7 @@ func newAccount(t *testing.T, tnt *data.Tenant, enc *enclave.Enclave, issuer str
}
payload := &proto.AccountData{
ProjectID: tnt.ProjectID,
UserID: fmt.Sprintf("%d|%s", 1, wallet.Address()),
UserID: fmt.Sprintf("%d|%s", tnt.ProjectID, signingSession.SessionID()),
Identity: identity.String(),
CreatedAt: time.Now(),
}
Expand Down Expand Up @@ -380,6 +382,15 @@ func newSession(t *testing.T, tnt *data.Tenant, enc *enclave.Enclave, issuer str
return newSessionFromData(t, tnt, enc, payload)
}

func unmarshalResponse[T any](t *testing.T, data any) *T {
b, err := json.Marshal(data)
require.NoError(t, err)

var res T
require.NoError(t, json.Unmarshal(b, &res))
return &res
}

type walletServiceMock struct {
registeredUsers map[string]struct{}
registeredSessions map[string]struct{}
Expand Down
101 changes: 101 additions & 0 deletions rpc/identity_provider.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
package rpc

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/0xsequence/go-sequence/intents"
"github.com/0xsequence/waas-authenticator/data"
"github.com/0xsequence/waas-authenticator/proto"
"github.com/0xsequence/waas-authenticator/rpc/crypto"
"github.com/0xsequence/waas-authenticator/rpc/signing"
"github.com/0xsequence/waas-authenticator/rpc/tenant"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
)

type openidConfig struct {
Expand Down Expand Up @@ -54,3 +67,91 @@ func (s *RPC) handleJWKS(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(pkd)
}

func (s *RPC) getIDToken(
ctx context.Context, sess *data.Session, intent *intents.IntentTyped[intents.IntentDataGetIdToken],
) (*intents.IntentResponseIdToken, error) {
tnt := tenant.FromContext(ctx)

if len(intent.Data.Nonce) > 128 {
return nil, fmt.Errorf("invalid nonce")
}

sessData, _, err := crypto.DecryptData[*proto.SessionData](ctx, sess.EncryptedKey, sess.Ciphertext, tnt.KMSKeys)
if err != nil {
return nil, err
}

var identity proto.Identity
if err := identity.FromString(sessData.Identity); err != nil {
return nil, fmt.Errorf("parsing session identity: %w", err)
}

acc, found, err := s.Accounts.Get(ctx, tnt.ProjectID, identity)
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("account not found")
}

walletAddr, err := AddressForUser(ctx, tnt, acc.UserID)
if err != nil {
return nil, fmt.Errorf("getting wallet address: %w", err)
}

aud := fmt.Sprintf("%s/project/%d", s.Config.Builder.BaseURL, tnt.ProjectID)
iat := time.Now()
exp := iat.Add(10 * time.Minute)

b := jwt.NewBuilder().
Subject(walletAddr).
Audience([]string{aud}).
Issuer(s.Config.BaseURL).
IssuedAt(iat).
Expiration(exp).
Claim("auth_time", sessData.CreatedAt.Unix()).
Claim(s.Config.BaseURL+"/identity", identity)

if acc.Email != "" {
b.Claim("email", acc.Email)
}

if intent.Data.Nonce != "" {
b.Claim("nonce", intent.Data.Nonce)
}

tok, err := b.Build()
if err != nil {
return nil, err
}

serialized, err := jwt.NewSerializer().Serialize(tok)
if err != nil {
return nil, err
}

// these can't fail, thus we ignore the errors
h := jws.NewHeaders()
_ = h.Set(jws.AlgorithmKey, jwa.RS256)
_ = h.Set(jws.KeyIDKey, s.Signer.KeyID())
_ = h.Set(jws.TypeKey, "JWT")

mh, err := json.Marshal(h)
if err != nil {
return nil, err
}

payload := base64.RawURLEncoding.EncodeToString(mh) + "." + base64.RawURLEncoding.EncodeToString(serialized)
signature, err := s.Signer.Sign(ctx, signing.AlgorithmRsaPkcs1V15Sha256, []byte(payload))
if err != nil {
return nil, err
}

signed := payload + "." + base64.RawURLEncoding.EncodeToString(signature)
res := &intents.IntentResponseIdToken{
IdToken: signed,
ExpiresIn: int(exp.Sub(iat).Seconds()),
}
return res, nil
}
92 changes: 92 additions & 0 deletions rpc/identity_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package rpc_test

import (
"context"
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/0xsequence/ethkit/ethwallet"
"github.com/0xsequence/go-sequence/intents"
"github.com/0xsequence/waas-authenticator/proto"
"github.com/0xsequence/waas-authenticator/rpc"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRPC_SendIntent_GetIdToken(t *testing.T) {
ctx := context.Background()

issuer, _, closeJWKS := issueAccessTokenAndRunJwksServer(t, func(b *jwt.Builder, s string) {
b.Claim("email", "[email protected]").Claim("email_verified", true)
})
defer closeJWKS()

sessWallet, err := ethwallet.NewWalletFromRandomEntropy()
require.NoError(t, err)
signingSession := intents.NewSessionP256K1(sessWallet)

svc := initRPC(t)

tenant, tntData := newTenant(t, svc.Enclave, issuer)
acc := newAccount(t, tenant, svc.Enclave, issuer, sessWallet)
sess := newSession(t, tenant, svc.Enclave, issuer, signingSession)

walletAddr, err := rpc.AddressForUser(context.Background(), tntData, acc.UserID)
require.NoError(t, err)

require.NoError(t, svc.Tenants.Add(ctx, tenant))
require.NoError(t, svc.Accounts.Put(ctx, acc))
require.NoError(t, svc.Sessions.Put(ctx, sess))

srv := httptest.NewServer(svc.Handler())
defer srv.Close()
svc.Config.BaseURL = srv.URL
svc.Config.Builder.BaseURL = "https://sequence.build"

intentData := &intents.IntentDataGetIdToken{
Wallet: walletAddr,
SessionID: sess.ID,
Nonce: "NONCE",
}
intent := generateSignedIntent(t, intents.IntentName_getIdToken, intentData, signingSession)

c := proto.NewWaasAuthenticatorClient(srv.URL, http.DefaultClient)
header := make(http.Header)
header.Set("X-Sequence-Project", strconv.Itoa(int(tenant.ProjectID)))
ctx, err = proto.WithHTTPRequestHeaders(ctx, header)
require.NoError(t, err)

res, err := c.SendIntent(ctx, intent)
require.NoError(t, err)
assert.Equal(t, proto.IntentResponseCode_idToken, res.Code)
assert.NotEmpty(t, res.Data)

resData := unmarshalResponse[intents.IntentResponseIdToken](t, res.Data)

jwks, err := jwk.Fetch(context.Background(), srv.URL+"/.well-known/jwks.json")
require.NoError(t, err)

opts := []jwt.ParseOption{
jwt.WithKeySet(jwks),
jwt.WithIssuer(srv.URL),
jwt.WithAudience("https://sequence.build/project/" + strconv.Itoa(int(tenant.ProjectID))),
jwt.WithSubject(walletAddr),
jwt.WithClaimValue("nonce", "NONCE"),
jwt.WithClaimValue("email", "[email protected]"),
}
tok, err := jwt.Parse([]byte(resData.IdToken), opts...)
require.NoError(t, err)
require.NotNil(t, tok)

identClaim, ok := tok.Get(srv.URL + "/identity")
require.True(t, ok)
identMap, ok := identClaim.(map[string]any)
require.True(t, ok, "should be a map, is %+v", identClaim)
assert.Equal(t, "OIDC", identMap["type"])
assert.Equal(t, issuer, identMap["iss"])
assert.Equal(t, "SUBJECT", identMap["sub"])
}
11 changes: 11 additions & 0 deletions rpc/intents.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,17 @@ func (s *RPC) SendIntent(ctx context.Context, protoIntent *proto.Intent) (*proto
return nil, err
}
return makeIntentResponse(proto.IntentResponseCode_accountRemoved, true), nil

case intents.IntentName_getIdToken:
intentTyped, err := intents.NewIntentTypedFromIntent[intents.IntentDataGetIdToken](intent)
if err != nil {
return nil, err
}
res, err := s.getIDToken(ctx, sess, intentTyped)
if err != nil {
return nil, err
}
return makeIntentResponse(proto.IntentResponseCode_idToken, res), nil
}

// Generic forwarding of intent, no special handling
Expand Down

0 comments on commit e5712a2

Please sign in to comment.