Skip to content

Commit

Permalink
Merge pull request #14 from passageidentity/users/db/ch1295-jwks-support
Browse files Browse the repository at this point in the history
[MAIN] JWKS Support
  • Loading branch information
dylanbrookes authored May 13, 2022
2 parents 679e812 + 75e3d7b commit d956cd4
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 62 deletions.
58 changes: 6 additions & 52 deletions app.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package passage

import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"net/http"

jwkLibrary "github.com/lestrrat-go/jwx/jwk"
"gopkg.in/resty.v1"
)

Expand All @@ -18,9 +15,9 @@ type Config struct {
}

type App struct {
ID string
PublicKey *rsa.PublicKey
Config *Config
ID string
JWKS jwkLibrary.Set
Config *Config
}

func New(appID string, config *Config) (*App, error) {
Expand All @@ -33,59 +30,16 @@ func New(appID string, config *Config) (*App, error) {
Config: config,
}

// Lookup the public key for this App:
var err error
app.PublicKey, err = getPublicKey(appID)
app.JWKS, err = app.fetchJWKS()
if err != nil {
return nil, err
}

return &app, nil
}

var publicKeyCache map[string]*rsa.PublicKey = make(map[string]*rsa.PublicKey)

func getPublicKey(appID string) (*rsa.PublicKey, error) {
// First, check if the App's public key is cached locally:
if cachedPublicKey, ok := publicKeyCache[appID]; ok {
return cachedPublicKey, nil
}

// If the public key isn't cached locally, we'll need to use the Passage API to lookup the public key:
var responseData struct {
App struct {
PublicKey string `json:"rsa_public_key"`
} `json:"app"`
}
response, err := resty.New().R().
SetResult(&responseData).
Get("https://api.passage.id/v1/apps/" + appID)
if err != nil {
return nil, errors.New("network error: could not lookup Passage App's public key")
}
if response.StatusCode() == http.StatusNotFound {
return nil, fmt.Errorf("passage App with ID \"%v\" does not exist", appID)
}
if response.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("failed to get lookup Passage App's public key")
}

// Parse the returned public key string to an rsa.PublicKey:
publicKeyBytes, err := base64.RawURLEncoding.DecodeString(responseData.App.PublicKey)
if err != nil {
return nil, errors.New("could not parse Passage App's public key: expected valid base-64")
}
pemBlock, _ := pem.Decode(publicKeyBytes)
if pemBlock == nil {
return nil, errors.New("could not parse Passage App's public key: missing PEM block")
}
publicKey, err := x509.ParsePKCS1PublicKey(pemBlock.Bytes)
if err != nil {
return nil, errors.New("could not parse Passage App's public key: invalid PKCS #1 public key")
}

return publicKey, nil
}
var jwkCache map[string]jwkLibrary.Set = make(map[string]jwkLibrary.Set)

type ChannelType string

Expand Down
47 changes: 40 additions & 7 deletions authentication.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package passage

import (
"context"
"errors"
"fmt"
"net/http"
"strings"

"github.com/golang-jwt/jwt"
jwkLibrary "github.com/lestrrat-go/jwx/jwk"
)

// AuthenticateRequest determines whether or not to authenticate via header or cookie authentication
Expand Down Expand Up @@ -33,6 +36,42 @@ func (a *App) AuthenticateRequestWithHeader(r *http.Request) (string, error) {
return userID, nil
}

// getPublicKey returns the associated public key for a JWT
func (a *App) getPublicKey(token *jwt.Token) (interface{}, error) {
keyID, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("expecting JWT header to have string kid")
}

key, ok := jwkCache[a.ID].LookupKeyID(keyID)
// if key doesn't exist, re-fetch one more time to see if this jwk was just added
if !ok {
a.fetchJWKS()
key, ok := jwkCache[a.ID].LookupKeyID(keyID)
if !ok {
return nil, fmt.Errorf("unable to find key %q", keyID)
}

var pubKey interface{}
err := key.Raw(&pubKey)
return pubKey, err
}

var pubKey interface{}
err := key.Raw(&pubKey)
return pubKey, err
}

// fetchJWKS returns the JWKS for the current app
func (a *App) fetchJWKS() (jwkLibrary.Set, error) {
jwks, err := jwkLibrary.Fetch(context.Background(), fmt.Sprintf("https://auth.passage.id/v1/apps/%v/.well-known/jwks.json", a.ID))
if err != nil {
return nil, errors.New("failed to fetch jwks")
}
jwkCache[a.ID] = jwks
return jwks, nil
}

// AuthenticateRequestWithCookie fetches a cookie from the request and uses it to authenticate
// returns the userID (string) on success, error on failure
func (a *App) AuthenticateRequestWithCookie(r *http.Request) (string, error) {
Expand All @@ -52,13 +91,7 @@ func (a *App) AuthenticateRequestWithCookie(r *http.Request) (string, error) {
// ValidateAuthToken determines whether a JWT is valid or not
// returns userID (string) on success, error on failure
func (a *App) ValidateAuthToken(authToken string) (string, bool) {
// Verify that the authentication token is valid:
parsedToken, err := jwt.Parse(authToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, errors.New("invalid signing algorithm")
}
return a.PublicKey, nil
})
parsedToken, err := jwt.Parse(authToken, a.getPublicKey)
if err != nil {
return "", false
}
Expand Down
21 changes: 21 additions & 0 deletions authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ func TestAuthenticationWithCookie(t *testing.T) {
req, err := http.NewRequest("GET", "https://example.com", nil)
require.Nil(t, err)

psg, err := passage.New("TrWSUbDDTPCKTQDtLA9MO8Ee", &passage.Config{
HeaderAuth: false,
})
require.Nil(t, err)

t.Run("valid auth token", func(t *testing.T) {
req.AddCookie(&http.Cookie{
Name: "psg_auth_token",
Value: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjlGVGdhaWpXV2hBUFhyTmJNQmMxc1lxWCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJleHAiOjE2ODg0OTA5NzcsImlhdCI6MTY1MjIwMjk3NywiaXNzIjoiVHJXU1ViRERUUENLVFFEdExBOU1POEVlIiwibmJmIjoxNjUyMjAyOTc3LCJzdWIiOiJiRVhJWktZeUFwZ3o1b1dZYzVXTTl2ZkYifQ.RFDUgv4ewmeEnapatQONuNJuofuTKDC7r7gZuvPGWpoX_EJWCgjVuysVt4L8ghUO_ZUuaujEn7loSZAtVVG7NKmivN2hSfCtCoK6JW-y8fn3izlaERl5fldkNdN8rxISlgqtANuPV0xfxtbIoqagV9wCAt2DY53HXDYM13ZRHIDrXgRO3-kiPhp_mO_tUnvHBRZ59DDFd-nqk99ssepT0-uEl-KVcHIQKbt5SfGgM9sR-b30mp6g-PkDDgdpMmS-ZLCNAZTkDclHTlCEdxCTdHS46z6yz6QAVgzhU0Z48q6olBzBEMGn-OC0fbwkYjG-j6xyWhpri0BamAG5I-i5Nw",
})

userID, err := psg.AuthenticateRequest(req)
require.Nil(t, err)
assert.NotNil(t, userID)
})
}

func TestAuthenticationWithCookiePassage(t *testing.T) {
req, err := http.NewRequest("GET", "https://example.com", nil)
require.Nil(t, err)

psg, err := passage.New("passage", &passage.Config{
HeaderAuth: false,
})
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.15
require (
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/joho/godotenv v1.4.0
github.com/stretchr/testify v1.7.0
github.com/lestrrat-go/jwx v1.2.24
github.com/stretchr/testify v1.7.1
gopkg.in/resty.v1 v1.12.0
)
36 changes: 34 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,19 +1,51 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.24 h1:N6Qsn6TUsDzz+qgS/1xcfBtkQfnbwW01fLFJpuYgKsg=
github.com/lestrrat-go/jwx v1.2.24/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 comments on commit d956cd4

Please sign in to comment.