Skip to content

Commit

Permalink
Draft: Asymmetric Key auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Danylo Patsora committed Oct 8, 2021
1 parent 9dfe6fe commit 886e46d
Show file tree
Hide file tree
Showing 13 changed files with 712 additions and 36 deletions.
10 changes: 8 additions & 2 deletions api/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ func (ts *AdminTestSuite) makeSuperAdmin(email string) string {
identities, err := models.FindIdentitiesByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")

token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")

token, err := generateAccessToken(u, identities, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
require.NoError(ts.T(), err, "Error generating access token")

p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
Expand All @@ -76,7 +79,10 @@ func (ts *AdminTestSuite) makeSystemUser() string {
identities, err := models.FindIdentitiesByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")

token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")

token, err := generateAccessToken(u, identities, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
require.NoError(ts.T(), err, "Error generating access token")

p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
Expand Down
2 changes: 2 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
sharedLimiter := api.limitEmailSentHandler()
r.With(sharedLimiter).With(api.requireAdminCredentials).Post("/invite", api.Invite)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/signup", api.Signup)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/sign_challenge", api.GetChallengeToken)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/asymmetric_login", api.SignInWithAsymmetricKey)
r.With(sharedLimiter).With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)

Expand Down
168 changes: 168 additions & 0 deletions api/asymmetric_signin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package api

import (
"encoding/json"
"github.com/gofrs/uuid"
"github.com/netlify/gotrue/models"
"net/http"

"github.com/netlify/gotrue/storage"
)

//signature 0xbd814e4f92afd63be08893be8bf7f88c0566ad6991eb9a1e81d81bbc8173233615ff9705fda470bb23b72a1c308768ebf61fc245a8e955417b201ac56d9243dc1c
// recoveredPubkey 0x048fe530980a230c6c9077418e80ac0119eae6e8b0f4157b9974337c983c685bc25af19f423ddaf7229d714bbb09848dc276bcab8c3ef34fdf8276da2ff8946135
// recoveredAddress 0x6BE46d7D863666546b77951D5dfffcF075F36E68

//signature 0x84be2bca9f4ab3ffa5158f0ab857987ad5e286a4b32f98c22c6d74659cff9726777ca74f7469c03b08301cbe3a1c381ece2c57249cfc6e4a0584dcff4b28d73b1c
// recoveredPubkey 0x048fe530980a230c6c9077418e80ac0119eae6e8b0f4157b9974337c983c685bc25af19f423ddaf7229d714bbb09848dc276bcab8c3ef34fdf8276da2ff8946135
// recoveredAddress 0x6BE46d7D863666546b77951D5dfffcF075F36E68

// GetChallengeTokenParams are the parameters the Signup endpoint accepts
type GetChallengeTokenParams struct {
Key string `json:"key"`
Algorithm string `json:"algorithm"`
}

// GetChallengeTokenParams are the parameters the Signup endpoint accepts
type GetChallengeTokenResponse struct {
ChallengeToken string `json:"challenge_token"`
}

func (a *API) GetChallengeToken(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.getConfig(ctx)

params := &GetChallengeTokenParams{}
jsonDecoder := json.NewDecoder(r.Body)
err := jsonDecoder.Decode(params)
if err != nil {
return badRequestError("Could not read GetChallengeTokenParams params: %v", err)
}

err = models.VerifyKeyAndAlgorithm(params.Key, params.Algorithm)
if err != nil {
return unprocessableEntityError("Key verification failed: %v", err)
}

user, key, err := models.FindUserWithAsymmetrickey(a.db, params.Key)
var challengeToken uuid.UUID

if err != nil && !models.IsNotFoundError(err) {
return internalServerError("Database error finding user").WithInternalError(err)
}

err = a.db.Transaction(func(tx *storage.Connection) error {
var terr error
if user != nil && key != nil {
challengeToken, terr = key.GetChallengeToken(tx)
if terr != nil {
return terr
}
} else if user == nil && key == nil {
if config.DisableSignup {
return forbiddenError("Signups not allowed for this instance")
}

user, terr = a.signupNewUser(ctx, tx, &SignupParams{
Email: "",
Phone: "",
Password: "",
Data: nil,
Provider: "AsymmetricKey",
})
if terr != nil {
return terr
}

key, terr = models.NewAssymetricKey(user.ID, params.Key, params.Algorithm, true)
if terr != nil {
return terr
}

if terr := tx.Create(key); terr != nil {
return terr
}

challengeToken, terr = key.GetChallengeToken(tx)
if terr != nil {
return terr
}
} else {
return internalServerError("Impossible case")
}
return nil
})

if err != nil {
return err
}

return sendJSON(w, http.StatusOK, GetChallengeTokenResponse{ChallengeToken: challengeToken.String()})
}

// AsymmetricSignInParams are the parameters the Signin endpoint accepts
type AsymmetricSignInParams struct {
Key string `json:"key"`
ChallengeTokenSignature string `json:"challenge_token_signature"`
}

type AsymmetricSignInResponse struct {
Passed bool `json:"true"`
}

func (a *API) SignInWithAsymmetricKey(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.getConfig(ctx)
cookie := r.Header.Get(useCookieHeader)

params := &AsymmetricSignInParams{}
jsonDecoder := json.NewDecoder(r.Body)
err := jsonDecoder.Decode(params)
if err != nil {
return badRequestError("Could not read AsymmetricSignInParams params: %v", err)
}

user, key, err := models.FindUserWithAsymmetrickey(a.db, params.Key)
if err != nil && models.IsNotFoundError(err) {
return unauthorizedError("Unauthorized")
}
if err != nil && !models.IsNotFoundError(err) {
return internalServerError("Database error finding key").WithInternalError(err)
}

if key.IsChallengeTokenExpired() {
return unprocessableEntityError("Key challenge token has been expired")
}

if err = key.VerifySignature(params.ChallengeTokenSignature); err != nil {
return unprocessableEntityError("Signature verification failed:%v", err)
}

var token *AccessTokenResponse
err = a.db.Transaction(func(tx *storage.Connection) error {
var terr error
terr = tx.UpdateOnly(key, "challenge_passed")
if terr != nil {
return terr
}

token, terr = a.issueRefreshToken(ctx, tx, user)
if terr != nil {
return terr
}

if cookie != "" && config.Cookie.Duration > 0 {
if terr = a.setCookieToken(config, token.Token, cookie == useSessionCookie, w); terr != nil {
return internalServerError("Failed to set JWT cookie. %s", terr)
}
}
return nil
})

if err != nil {
return err
}

token.User = user
return sendJSON(w, http.StatusOK, token)
}
6 changes: 5 additions & 1 deletion api/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string {

identities, err := models.FindIdentitiesByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")
token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)

key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")

token, err := generateAccessToken(u, identities, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
require.NoError(ts.T(), err, "Error generating access token")

p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
Expand Down
6 changes: 5 additions & 1 deletion api/invite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string {

identities, err := models.FindIdentitiesByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")
token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)

key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")

token, err := generateAccessToken(u, identities, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
require.NoError(ts.T(), err, "Error generating access token")

p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
Expand Down
40 changes: 29 additions & 11 deletions api/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ type GoTrueClaims struct {
Phone string `json:"phone"`
AppMetaData map[string]interface{} `json:"app_metadata"`
UserMetaData map[string]interface{} `json:"user_metadata"`
Identities []*models.Identity `json:"identities"`
Role string `json:"role"`

MainAsymmetricKey string `json:"asymmetric_key"`
MainAsymmetricKeyAlgorithm string `json:"asymmetric_key_algorithm"`

Identities []*models.Identity `json:"identities"`
Role string `json:"role"`
}

// AccessTokenResponse represents an OAuth2 success response
Expand Down Expand Up @@ -191,7 +195,12 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
return internalServerError("error retrieving identities").WithInternalError(terr)
}

tokenString, terr = generateAccessToken(user, identities, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
key, terr := models.FindMainAsymmetricKeyByUser(tx, user)
if terr != nil {
return internalServerError("Database error granting user").WithInternalError(terr)
}

tokenString, terr = generateAccessToken(user, identities, key, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
if terr != nil {
return internalServerError("error generating jwt token").WithInternalError(terr)
}
Expand All @@ -216,19 +225,24 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
})
}

func generateAccessToken(user *models.User, identities []*models.Identity, expiresIn time.Duration, secret string) (string, error) {
func generateAccessToken(user *models.User, identities []*models.Identity, key *models.AsymmetricKey, expiresIn time.Duration, secret string) (string, error) {
claims := &GoTrueClaims{
StandardClaims: jwt.StandardClaims{
Subject: user.ID.String(),
Audience: user.Aud,
ExpiresAt: time.Now().Add(expiresIn).Unix(),
},
Email: user.GetEmail(),
Phone: user.GetPhone(),
AppMetaData: user.AppMetaData,
UserMetaData: user.UserMetaData,
Identities: identities,
Role: user.Role,
Email: user.GetEmail(),
Phone: user.GetPhone(),
AppMetaData: user.AppMetaData,
UserMetaData: user.UserMetaData,
Identities: identities,
Role: user.Role,
}

if key != nil {
claims.MainAsymmetricKey = key.Key
claims.MainAsymmetricKeyAlgorithm = key.Algorithm
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
Expand All @@ -254,8 +268,12 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u
if terr != nil {
return internalServerError("Database error granting user").WithInternalError(terr)
}
key, terr := models.FindMainAsymmetricKeyByUser(tx, user)
if terr != nil {
return internalServerError("Database error granting user").WithInternalError(terr)
}

tokenString, terr = generateAccessToken(user, identities, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
tokenString, terr = generateAccessToken(user, identities, key, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
if terr != nil {
return internalServerError("error generating jwt token").WithInternalError(terr)
}
Expand Down
5 changes: 4 additions & 1 deletion api/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ func (ts *UserTestSuite) TestUser_UpdatePassword() {
identities, err := models.FindIdentitiesByUser(ts.API.db, u)
require.NoError(ts.T(), err)

token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u)
require.NoError(ts.T(), err, "Error retrieving identities")

token, err := generateAccessToken(u, identities, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
require.NoError(ts.T(), err)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))

Expand Down
12 changes: 4 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62
github.com/beevik/etree v1.1.0
github.com/didip/tollbooth/v5 v5.1.1
github.com/ethereum/go-ethereum v1.10.9
github.com/fatih/color v1.10.0 // indirect
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-sql-driver/mysql v1.5.0
Expand Down Expand Up @@ -36,22 +37,17 @@ require (
github.com/opentracing/opentracing-go v1.1.0
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.9.1
github.com/rs/cors v1.6.0
github.com/rs/cors v1.7.0
github.com/russellhaering/gosaml2 v0.6.0
github.com/russellhaering/goxmldsig v1.1.1
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35
github.com/sethvargo/go-password v0.2.0
github.com/sirupsen/logrus v1.7.0
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.12.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
Expand Down
Loading

0 comments on commit 886e46d

Please sign in to comment.