Skip to content

exp/services/webauth: add SEP-10 v1.2.0 implementation #2074

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

Merged
merged 31 commits into from
Dec 20, 2019
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
38b769c
exp/services/webauth: add SEP-10 implementation
leighmcculloch Dec 10, 2019
b738015
Revert back to what is required by current SEP-10
leighmcculloch Dec 18, 2019
41ea106
feedback from @ire-and-cursers
leighmcculloch Dec 18, 2019
4f60806
Change network default to test to align with horizon url
leighmcculloch Dec 18, 2019
d19b9c4
Add README
leighmcculloch Dec 18, 2019
a5ce7fa
Remove go1.13 specific dependency
leighmcculloch Dec 18, 2019
fa8c22c
Move jwtkey to an exp package
leighmcculloch Dec 18, 2019
223e13b
Move jwtkey to an exp/support package
leighmcculloch Dec 18, 2019
fa66f33
Add links and why to readme
leighmcculloch Dec 18, 2019
5e8bf2e
support/http/httpdecode: add form decoding helper
leighmcculloch Dec 19, 2019
dbe1522
support/http/httpdecode: add form+json combined decoding helper
leighmcculloch Dec 19, 2019
78e6d1d
Merge branch 'httpdecode-formurlencoded' into sep10webauth-base
leighmcculloch Dec 19, 2019
d172565
Use httpdecode
leighmcculloch Dec 19, 2019
d014da9
Remove unnecessary checks
leighmcculloch Dec 19, 2019
69b05e2
Fix duration in application options
leighmcculloch Dec 19, 2019
f5575e5
Fix unnecessary import
leighmcculloch Dec 19, 2019
1f0457d
Update README
leighmcculloch Dec 19, 2019
271b5aa
Rename Main to Run
leighmcculloch Dec 19, 2019
a45dfe6
Rename Run to Serve
leighmcculloch Dec 19, 2019
b5a7734
Merge branch 'master' into sep10webauth-base
leighmcculloch Dec 19, 2019
5beab66
Remove unused error
leighmcculloch Dec 20, 2019
03dbca2
Fix logger log ref
leighmcculloch Dec 20, 2019
97d33e5
Put jwtkeygen into a separate tool and link it into the server
leighmcculloch Dec 20, 2019
6aba5cc
Revert "Put jwtkeygen into a separate tool and link it into the server"
leighmcculloch Dec 20, 2019
a1a8106
Wrap error
leighmcculloch Dec 20, 2019
6b7e851
Add missing return
leighmcculloch Dec 20, 2019
8a11519
Keep logging in one function and pass around errors instead
leighmcculloch Dec 20, 2019
79c0fee
Add http.Status* instead of numbers
leighmcculloch Dec 20, 2019
6dc1db5
Add http.Status* instead of numbers
leighmcculloch Dec 20, 2019
abad794
Add logic to check master key is a signer with high threshold
leighmcculloch Dec 20, 2019
100b32f
Update README
leighmcculloch Dec 20, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions exp/services/webauth/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# webauth

This is a SEP-10 Web Authentication implementation based on SEP-10 v1.2.0.
This is a [SEP-10] Web Authentication implementation based on SEP-10 v1.2.0
that requires the master key have a high threshold for authentication to
succeed.

This implementation is not polished and is still experimental. Running this implementation in production is not recommended.
SEP-10 defines an endpoint for authenticating a user in possession of a Stellar
account using their Stellar account as credentials. This implementation is a
standalone microservice that implements the minimum requirements as defined by
the SEP-10 protocol and will be adapted as the protocol evolves.

This implementation is not polished and is still experimental.
Running this implementation in production is not recommended.

## Usage

Expand Down Expand Up @@ -34,11 +42,13 @@ Usage:
webauth serve [flags]

Flags:
--challenge-expires-in int The time period after which the challenge transaction expires (default 300000000000)
--challenge-expires-in int The time period in seconds after which the challenge transaction expires (default 300)
--horizon-url string Horizon URL used for looking up account details (default "https://horizon-testnet.stellar.org/")
--jwt-expires-in int The time period that after which the JWT expires (default 18000000000000)
--jwt-expires-in int The time period in seconds after which the JWT expires (default 300)
--jwt-key string Base64 encoded ECDSA private key used for signing JWTs
--network-passphrase string Network passphrase of the Stellar network transactions should be signed for (default "Test SDF Network ; September 2015")
--port int Port to listen and serve on (default 8000)
--signing-key string Stellar signing key used for signing transactions
```

[SEP-10]: https://github.com/stellar/stellar-protocol/blob/2be91ce8d8032ca9b2f368800d06b9fba346a147/ecosystem/sep-0010.md
11 changes: 5 additions & 6 deletions exp/services/webauth/internal/commands/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package commands

import (
"go/types"
"time"

"github.com/spf13/cobra"
"github.com/stellar/go/clients/horizonclient"
Expand Down Expand Up @@ -54,11 +53,11 @@ func (c *ServeCommand) Command() *cobra.Command {
},
{
Name: "challenge-expires-in",
Usage: "The time period after which the challenge transaction expires",
Usage: "The time period in seconds after which the challenge transaction expires",
OptType: types.Int,
CustomSetValue: config.SetDuration,
ConfigKey: &opts.ChallengeExpiresIn,
FlagDefault: int(300 * time.Second),
FlagDefault: 300,
Required: true,
},
{
Expand All @@ -70,11 +69,11 @@ func (c *ServeCommand) Command() *cobra.Command {
},
{
Name: "jwt-expires-in",
Usage: "The time period that after which the JWT expires",
Usage: "The time period in seconds after which the JWT expires",
OptType: types.Int,
CustomSetValue: config.SetDuration,
ConfigKey: &opts.JWTExpiresIn,
FlagDefault: int(300 * time.Minute),
FlagDefault: 300,
Required: true,
},
}
Expand All @@ -92,5 +91,5 @@ func (c *ServeCommand) Command() *cobra.Command {
}

func (c *ServeCommand) Run(opts serve.Options) {
serve.Main(opts)
serve.Serve(opts)
}
14 changes: 7 additions & 7 deletions exp/services/webauth/internal/serve/challenge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package serve
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
Expand All @@ -16,9 +17,8 @@ import (
)

func TestChallenge(t *testing.T) {
serverKey, err := keypair.Random()
require.NoError(t, err)
account, err := keypair.Random()
serverKey := keypair.MustRandom()
account := keypair.MustRandom()

h := challengeHandler{
Logger: supportlog.DefaultLogger,
Expand All @@ -33,14 +33,14 @@ func TestChallenge(t *testing.T) {
h.ServeHTTP(w, r)
resp := w.Result()

require.Equal(t, 200, resp.StatusCode)
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "application/json; charset=utf-8", resp.Header.Get("Content-Type"))

res := struct {
Transaction string `json:"transaction"`
NetworkPassphrase string `json:"network_passphrase"`
}{}
err = json.NewDecoder(resp.Body).Decode(&res)
err := json.NewDecoder(resp.Body).Decode(&res)
require.NoError(t, err)

var tx xdr.TransactionEnvelope
Expand Down Expand Up @@ -71,7 +71,7 @@ func TestChallengeNoAccount(t *testing.T) {
h.ServeHTTP(w, r)
resp := w.Result()

require.Equal(t, 400, resp.StatusCode)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.Equal(t, "application/json; charset=utf-8", resp.Header.Get("Content-Type"))

body, err := ioutil.ReadAll(resp.Body)
Expand All @@ -87,7 +87,7 @@ func TestChallengeInvalidAccount(t *testing.T) {
h.ServeHTTP(w, r)
resp := w.Result()

require.Equal(t, 400, resp.StatusCode)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.Equal(t, "application/json; charset=utf-8", resp.Header.Get("Content-Type"))

body, err := ioutil.ReadAll(resp.Body)
Expand Down
4 changes: 0 additions & 4 deletions exp/services/webauth/internal/serve/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ var badRequest = errorResponse{
Status: http.StatusBadRequest,
Error: "The request was invalid in some way.",
}
var unsupportedMediaType = errorResponse{
Status: http.StatusUnsupportedMediaType,
Error: "The request is of content type or encoding that is unsupported.",
}
var unauthorized = errorResponse{
Status: http.StatusUnauthorized,
Error: "The request could not be authenticated.",
Expand Down
5 changes: 3 additions & 2 deletions exp/services/webauth/internal/serve/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package serve

import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

Expand All @@ -13,7 +14,7 @@ func TestErrorResponseRender(t *testing.T) {
w := httptest.NewRecorder()
serverError.Render(w)
resp := w.Result()
assert.Equal(t, 500, resp.StatusCode)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.JSONEq(t, `{"error":"An error occurred while processing this request."}`, string(body))
Expand All @@ -25,7 +26,7 @@ func TestErrorHandler(t *testing.T) {
handler := errorHandler{Error: notFound}
handler.ServeHTTP(w, r)
resp := w.Result()
assert.Equal(t, 404, resp.StatusCode)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.JSONEq(t, `{"error":"The resource at the url requested was not found."}`, string(body))
Expand Down
18 changes: 12 additions & 6 deletions exp/services/webauth/internal/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/exp/support/jwtkey"
"github.com/stellar/go/keypair"
"github.com/stellar/go/support/errors"
supporthttp "github.com/stellar/go/support/http"
supportlog "github.com/stellar/go/support/log"
)
Expand All @@ -23,8 +24,13 @@ type Options struct {
JWTExpiresIn time.Duration
}

func Main(opts Options) {
handler := handler(opts)
func Serve(opts Options) {
handler, err := handler(opts)
if err != nil {
opts.Logger.Fatalf("Error: %v", err)
return
}

addr := fmt.Sprintf(":%d", opts.Port)
supporthttp.Run(supporthttp.Config{
ListenAddr: addr,
Expand All @@ -36,15 +42,15 @@ func Main(opts Options) {
})
}

func handler(opts Options) http.Handler {
func handler(opts Options) (http.Handler, error) {
signingKey, err := keypair.ParseFull(opts.SigningKey)
if err != nil {
opts.Logger.Fatalf("Error parsing signing key seed: %v", err)
return nil, errors.Wrap(err, "parsing signing key seed")
}

jwtPrivateKey, err := jwtkey.PrivateKeyFromString(opts.JWTPrivateKey)
if err != nil {
opts.Logger.Fatalf("Error parsing JWT private key: %v", err)
return nil, errors.Wrap(err, "parsing JWT private key")
}

httpClient := &http.Client{
Expand Down Expand Up @@ -77,5 +83,5 @@ func handler(opts Options) http.Handler {
JWTExpiresIn: opts.JWTExpiresIn,
}.ServeHTTP)

return mux
return mux, nil
}
45 changes: 23 additions & 22 deletions exp/services/webauth/internal/serve/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package serve

import (
"crypto/ecdsa"
"encoding/json"
"net/http"
"time"

"github.com/dgrijalva/jwt-go"
"github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/keypair"
"github.com/stellar/go/support/http/httpdecode"
supportlog "github.com/stellar/go/support/log"

"github.com/stellar/go/support/render/httpjson"
Expand All @@ -25,7 +25,7 @@ type tokenHandler struct {
}

type tokenRequest struct {
Transaction string `json:"transaction"`
Transaction string `json:"transaction" form:"transaction"`
}

type tokenResponse struct {
Expand All @@ -37,29 +37,13 @@ func (h tokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

req := tokenRequest{}

contentType := r.Header.Get("Content-Type")
switch contentType {
case "application/x-www-form-urlencoded":
defer r.Body.Close()
err := r.ParseForm()
if err != nil {
badRequest.Render(w)
return
}
req.Transaction = r.PostForm.Get("transaction")
case "application/json", "application/json; charset=utf-8":
defer r.Body.Close()
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
badRequest.Render(w)
return
}
default:
unsupportedMediaType.Render(w)
err := httpdecode.Decode(r, &req)
if err != nil {
badRequest.Render(w)
return
}

_, err := txnbuild.VerifyChallengeTx(req.Transaction, h.SigningAddress.Address(), h.NetworkPassphrase)
_, err = txnbuild.VerifyChallengeTx(req.Transaction, h.SigningAddress.Address(), h.NetworkPassphrase)
if err != nil {
unauthorized.Render(w)
return
Expand All @@ -72,6 +56,23 @@ func (h tokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
clientAccountID := tx.Operations[0].(*txnbuild.ManageData).SourceAccount.GetAccountID()

clientAccount, err := h.HorizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: clientAccountID})
if err != nil {
serverError.Render(w)
return
}
verifiedWeight := false
for _, clientSigner := range clientAccount.Signers {
if clientSigner.Key == clientAccountID && clientSigner.Weight >= int32(clientAccount.Thresholds.HighThreshold) {
verifiedWeight = true
break
}
}
if !verifiedWeight {
unauthorized.Render(w)
return
}

now := time.Now().UTC()
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"iss": h.SigningAddress.Address(),
Expand Down
Loading