Link account id with the external user store (#184)

* get account id from access token claim

* use GetOrCreateAccountByUser and add test

* correct account id claim

* remove unused account

* Idp manager interface

* auth0 idp manager

* use if instead of switch case

* remove unnecessary lock

* NewAuth0Manager

* move idpmanager to its own package

* update metadata when accountId is not supplied

* update tests with idpmanager field

* format

* new idp manager and config support

* validate if we fetch the interface before converting to string

* split getJWTToken

* improve tests

* proper json fields and handle defer body close

* fix ci lint notes

* documentation and proper defer position

* UpdateUserAppMetadata tests

* update documentation

* ManagerCredentials interface

* Marshal and Unmarshal functions

* fix tests

* ManagerHelper and ManagerHTTPClient

* further tests with mocking

* rename package and custom http client

* sync local packages

* remove idp suffix
This commit is contained in:
Maycon Santos 2022-01-24 11:21:30 +01:00 committed by GitHub
parent 2ad899b066
commit fd7282d3cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 806 additions and 28 deletions

View File

@ -38,7 +38,7 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste
}
peersUpdateManager := mgmt.NewPeersUpdateManager()
accountManager := mgmt.NewManager(store, peersUpdateManager)
accountManager := mgmt.NewManager(store, peersUpdateManager, nil)
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {

View File

@ -442,7 +442,7 @@ func startManagement(port int, dataDir string) (*grpc.Server, error) {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()
accountManager := server.NewManager(store, peersUpdateManager)
accountManager := server.NewManager(store, peersUpdateManager, nil)
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {

1
go.sum
View File

@ -148,7 +148,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

View File

@ -35,5 +35,15 @@
"AuthIssuer": "https://$WIRETRUSTEE_AUTH0_DOMAIN/",
"AuthAudience": "$WIRETRUSTEE_AUTH0_AUDIENCE",
"AuthKeysLocation": "https://$WIRETRUSTEE_AUTH0_DOMAIN/.well-known/jwks.json"
}
},
"IdpManagerConfig": {
"Manager": "none",
"Auth0ClientCredentials": {
"Audience": "<PASTE YOUR AUTH0 AUDIENCE HERE>",
"AuthIssuer": "<PASTE YOUR AUTH0 Auth Issuer HERE>",
"ClientId": "<PASTE YOUR AUTH0 Application Client ID HERE>",
"ClientSecret": "<PASTE YOUR AUTH0 Application Client Secret HERE>",
"GrantType": "client_credentials"
}
}
}

View File

@ -45,7 +45,7 @@ docker run -d --name wiretrustee-management \
wiretrustee/management:latest \
--letsencrypt-domain <YOUR-DOMAIN>
```
> An example of config.json can be found here [config.json](../infrastructure_files/config.json)
> An example of config.json can be found here [management.json](../infrastructure_files/management.json.tmpl)
Trigger Let's encrypt certificate generation:
```bash

View File

@ -61,7 +61,7 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste
}
peersUpdateManager := mgmt.NewPeersUpdateManager()
accountManager := mgmt.NewManager(store, peersUpdateManager)
accountManager := mgmt.NewManager(store, peersUpdateManager, nil)
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {

View File

@ -7,6 +7,7 @@ import (
"fmt"
"github.com/wiretrustee/wiretrustee/management/server"
"github.com/wiretrustee/wiretrustee/management/server/http"
"github.com/wiretrustee/wiretrustee/management/server/idp"
"github.com/wiretrustee/wiretrustee/util"
"net"
"os"
@ -68,7 +69,11 @@ var (
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()
accountManager := server.NewManager(store, peersUpdateManager)
idpManager, err := idp.NewManager(*config.IdpManagerConfig)
if err != nil {
log.Fatalln("failed retrieving a new idp manager with err: ", err)
}
accountManager := server.NewManager(store, peersUpdateManager, idpManager)
var opts []grpc.ServerOption

View File

@ -3,6 +3,7 @@ package server
import (
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/management/server/idp"
"github.com/wiretrustee/wiretrustee/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@ -14,6 +15,7 @@ type AccountManager struct {
// mutex to synchronise account operations (e.g. generating Peer IP address inside the Network)
mux sync.Mutex
peersUpdateManager *PeersUpdateManager
idpManager idp.Manager
}
// Account represents a unique account of the system
@ -60,11 +62,12 @@ func (a *Account) Copy() *Account {
}
// NewManager creates a new AccountManager with a provided Store
func NewManager(store Store, peersUpdateManager *PeersUpdateManager) *AccountManager {
func NewManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager) *AccountManager {
return &AccountManager{
Store: store,
mux: sync.Mutex{},
peersUpdateManager: peersUpdateManager,
idpManager: idpManager,
}
}
@ -159,6 +162,30 @@ func (am *AccountManager) GetAccount(accountId string) (*Account, error) {
return account, nil
}
//GetAccountByUserOrAccountId look for an account by user or account Id, if no account is provided and
// user id doesn't have an account associated with it, one account is created
func (am *AccountManager) GetAccountByUserOrAccountId(userId, accountId string) (*Account, error) {
if accountId != "" {
return am.GetAccount(accountId)
} else if userId != "" {
account, err := am.GetOrCreateAccountByUser(userId)
if err != nil {
return nil, status.Errorf(codes.NotFound, "account not found using user id: %s", userId)
}
// update idp manager app metadata
if am.idpManager != nil {
err = am.idpManager.UpdateUserAppMetadata(userId, idp.AppMetadata{WTAccountId: account.Id})
if err != nil {
return nil, status.Errorf(codes.Internal, "updating user's app metadata failed with: %v", err)
}
}
return account, nil
}
return nil, status.Errorf(codes.NotFound, "no valid user or account Id provided")
}
//AccountExists checks whether account exists (returns true) or not (returns false)
func (am *AccountManager) AccountExists(accountId string) (*bool, error) {
am.mux.Lock()

View File

@ -70,6 +70,36 @@ func TestAccountManager_AddAccount(t *testing.T) {
}
}
func TestAccountManager_GetAccountByUserOrAccountId(t *testing.T) {
manager, err := createManager(t)
if err != nil {
t.Fatal(err)
return
}
userId := "test_user"
account, err := manager.GetAccountByUserOrAccountId(userId, "")
if err != nil {
t.Fatal(err)
}
if account == nil {
t.Fatalf("expected to create an account for a user %s", userId)
}
accountId := account.Id
_, err = manager.GetAccountByUserOrAccountId("", accountId)
if err != nil {
t.Errorf("expected to get existing account after creation using userid, no account was found for a account %s", accountId)
}
_, err = manager.GetAccountByUserOrAccountId("", "")
if err == nil {
t.Errorf("expected an error when user and account IDs are empty")
}
}
func TestAccountManager_AccountExists(t *testing.T) {
manager, err := createManager(t)
if err != nil {
@ -258,7 +288,7 @@ func createManager(t *testing.T) (*AccountManager, error) {
if err != nil {
return nil, err
}
return NewManager(store, NewPeersUpdateManager()), nil
return NewManager(store, NewPeersUpdateManager(), nil), nil
}
func createStore(t *testing.T) (Store, error) {

View File

@ -1,6 +1,7 @@
package server
import (
"github.com/wiretrustee/wiretrustee/management/server/idp"
"github.com/wiretrustee/wiretrustee/util"
)
@ -23,6 +24,8 @@ type Config struct {
Datadir string
HttpConfig *HttpServerConfig
IdpManagerConfig *idp.Config
}
// TURNConfig is a config of the TURNCredentialsManager

View File

@ -13,6 +13,7 @@ import (
//Peers is a handler that returns peers of the account
type Peers struct {
accountManager *server.AccountManager
authAudience string
}
//PeerResponse is a response sent to the client
@ -29,9 +30,10 @@ type PeerRequest struct {
Name string
}
func NewPeers(accountManager *server.AccountManager) *Peers {
func NewPeers(accountManager *server.AccountManager, authAudience string) *Peers {
return &Peers{
accountManager: accountManager,
authAudience: authAudience,
}
}
@ -62,8 +64,9 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW
}
func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
userId := extractUserIdFromRequestContext(r)
account, err := h.accountManager.GetOrCreateAccountByUser(userId)
userId, accountId := extractUserAndAccountIdFromRequestContext(r, h.authAudience)
//new user -> create a new account
account, err := h.accountManager.GetAccountByUserOrAccountId(userId, accountId)
if err != nil {
log.Errorf("failed getting account of a user %s: %v", userId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@ -102,9 +105,9 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
userId := extractUserIdFromRequestContext(r)
userId, accountId := extractUserAndAccountIdFromRequestContext(r, h.authAudience)
//new user -> create a new account
account, err := h.accountManager.GetOrCreateAccountByUser(userId)
account, err := h.accountManager.GetAccountByUserOrAccountId(userId, accountId)
if err != nil {
log.Errorf("failed getting account of a user %s: %v", userId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)

View File

@ -15,6 +15,7 @@ import (
// SetupKeys is a handler that returns a list of setup keys of the account
type SetupKeys struct {
accountManager *server.AccountManager
authAudience string
}
// SetupKeyResponse is a response sent to the client
@ -39,9 +40,10 @@ type SetupKeyRequest struct {
Revoked bool
}
func NewSetupKeysHandler(accountManager *server.AccountManager) *SetupKeys {
func NewSetupKeysHandler(accountManager *server.AccountManager, authAudience string) *SetupKeys {
return &SetupKeys{
accountManager: accountManager,
authAudience: authAudience,
}
}
@ -118,8 +120,8 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
}
func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
userId := extractUserIdFromRequestContext(r)
account, err := h.accountManager.GetOrCreateAccountByUser(userId)
userId, accountId := extractUserAndAccountIdFromRequestContext(r, h.authAudience)
account, err := h.accountManager.GetAccountByUserOrAccountId(userId, accountId)
if err != nil {
log.Errorf("failed getting account of a user %s: %v", userId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@ -147,9 +149,8 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
userId := extractUserIdFromRequestContext(r)
//new user -> create a new account
account, err := h.accountManager.GetOrCreateAccountByUser(userId)
userId, accountId := extractUserAndAccountIdFromRequestContext(r, h.authAudience)
account, err := h.accountManager.GetAccountByUserOrAccountId(userId, accountId)
if err != nil {
log.Errorf("failed getting account of a user %s: %v", userId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)

View File

@ -8,13 +8,17 @@ import (
"time"
)
// extractUserIdFromRequestContext extracts accountId from the request context previously filled by the JWT token (after auth)
func extractUserIdFromRequestContext(r *http.Request) string {
// extractUserAndAccountIdFromRequestContext extracts accountId from the request context previously filled by the JWT token (after auth)
func extractUserAndAccountIdFromRequestContext(r *http.Request, authAudiance string) (userId, accountId string) {
token := r.Context().Value("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
//actually a user id but for now we have a 1 to 1 mapping.
return claims["sub"].(string)
userId = claims["sub"].(string)
accountIdInt, ok := claims[authAudiance+"wt_account_id"]
if ok {
accountId = accountIdInt.(string)
}
return userId, accountId
}
//writeJSONObject simply writes object to the HTTP reponse in JSON format

View File

@ -73,8 +73,8 @@ func (s *Server) Start() error {
r := mux.NewRouter()
r.Use(jwtMiddleware.Handler, corsMiddleware.Handler)
peersHandler := handler.NewPeers(s.accountManager)
keysHandler := handler.NewSetupKeysHandler(s.accountManager)
peersHandler := handler.NewPeers(s.accountManager, s.config.AuthAudience)
keysHandler := handler.NewSetupKeysHandler(s.accountManager, s.config.AuthAudience)
r.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
r.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).Methods("GET", "PUT", "DELETE", "OPTIONS")

View File

@ -0,0 +1,207 @@
package idp
import (
"encoding/json"
"fmt"
"github.com/golang-jwt/jwt"
log "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
)
// Auth0Manager auth0 manager client instance
type Auth0Manager struct {
authIssuer string
httpClient ManagerHTTPClient
credentials ManagerCredentials
helper ManagerHelper
}
// Auth0ClientConfig auth0 manager client configurations
type Auth0ClientConfig struct {
Audience string `json:"audiance"`
AuthIssuer string `json:"auth_issuer"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
GrantType string `json:"grant_type"`
}
// Auth0Credentials auth0 authentication information
type Auth0Credentials struct {
clientConfig Auth0ClientConfig
helper ManagerHelper
httpClient ManagerHTTPClient
jwtToken JWTToken
mux sync.Mutex
}
// NewAuth0Manager creates a new instance of the Auth0Manager
func NewAuth0Manager(config Auth0ClientConfig) *Auth0Manager {
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
httpTransport.MaxIdleConns = 5
httpTransport.IdleConnTimeout = 30
httpClient := &http.Client{
Timeout: 10 * time.Second,
Transport: httpTransport,
}
helper := JsonParser{}
credentials := &Auth0Credentials{
clientConfig: config,
httpClient: httpClient,
helper: helper,
}
return &Auth0Manager{
authIssuer: config.AuthIssuer,
credentials: credentials,
httpClient: httpClient,
helper: helper,
}
}
// jwtStillValid returns true if the token still valid and have enough time to be used and get a response from Auth0
func (c *Auth0Credentials) jwtStillValid() bool {
return !c.jwtToken.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(c.jwtToken.expiresInTime)
}
// requestJWTToken performs request to get jwt token
func (c *Auth0Credentials) requestJWTToken() (*http.Response, error) {
var res *http.Response
url := c.clientConfig.AuthIssuer + "/oauth/token"
p, err := c.helper.Marshal(c.clientConfig)
if err != nil {
return res, err
}
payload := strings.NewReader(string(p))
req, err := http.NewRequest("POST", url, payload)
if err != nil {
return res, err
}
req.Header.Add("content-type", "application/json")
res, err = c.httpClient.Do(req)
if err != nil {
return res, err
}
if res.StatusCode != 200 {
return res, fmt.Errorf("unable to get token, statusCode %d", res.StatusCode)
}
return res, nil
}
// parseRequestJWTResponse parses jwt raw response body and extracts token and expires in seconds
func (c *Auth0Credentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JWTToken, error) {
jwtToken := JWTToken{}
body, err := ioutil.ReadAll(rawBody)
if err != nil {
return jwtToken, err
}
err = c.helper.Unmarshal(body, &jwtToken)
if err != nil {
return jwtToken, err
}
if jwtToken.ExpiresIn == 0 && jwtToken.AccessToken == "" {
return jwtToken, fmt.Errorf("error while reading response body, expires_in: %d and access_token: %s", jwtToken.ExpiresIn, jwtToken.AccessToken)
}
data, err := jwt.DecodeSegment(strings.Split(jwtToken.AccessToken, ".")[1])
if err != nil {
return jwtToken, err
}
// Exp maps into exp from jwt token
var IssuedAt struct{ Exp int64 }
err = json.Unmarshal(data, &IssuedAt)
if err != nil {
return jwtToken, err
}
jwtToken.expiresInTime = time.Unix(IssuedAt.Exp, 0)
return jwtToken, nil
}
// Authenticate retrieves access token to use the Auth0 Management API
func (c *Auth0Credentials) Authenticate() (JWTToken, error) {
c.mux.Lock()
defer c.mux.Unlock()
// If jwtToken has an expires time and we have enough time to do a request return immediately
if c.jwtStillValid() {
return c.jwtToken, nil
}
res, err := c.requestJWTToken()
if err != nil {
return c.jwtToken, err
}
defer func() {
err = res.Body.Close()
if err != nil {
log.Errorf("error while closing get jwt token response body: %v", err)
}
}()
jwtToken, err := c.parseRequestJWTResponse(res.Body)
if err != nil {
return c.jwtToken, err
}
c.jwtToken = jwtToken
return c.jwtToken, nil
}
// UpdateUserAppMetadata updates user app metadata based on userId and metadata map
func (am *Auth0Manager) UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error {
jwtToken, err := am.credentials.Authenticate()
if err != nil {
return err
}
url := am.authIssuer + "/api/v2/users/" + userId
data, err := am.helper.Marshal(appMetadata)
if err != nil {
return err
}
payloadString := fmt.Sprintf("{\"app_metadata\": %s}", string(data))
payload := strings.NewReader(payloadString)
req, err := http.NewRequest("PATCH", url, payload)
if err != nil {
return err
}
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
req.Header.Add("content-type", "application/json")
res, err := am.httpClient.Do(req)
if err != nil {
return err
}
defer func() {
err = res.Body.Close()
if err != nil {
log.Errorf("error while closing update user app metadata response body: %v", err)
}
}()
if res.StatusCode != 200 {
return fmt.Errorf("unable to update the appMetadata, statusCode %d", res.StatusCode)
}
return nil
}

View File

@ -0,0 +1,403 @@
package idp
import (
"encoding/json"
"fmt"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/assert"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
)
type mockHTTPClient struct {
code int
resBody string
reqBody string
err error
}
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
body, err := ioutil.ReadAll(req.Body)
if err == nil {
c.reqBody = string(body)
}
return &http.Response{
StatusCode: c.code,
Body: ioutil.NopCloser(strings.NewReader(c.resBody)),
}, c.err
}
type mockJsonParser struct {
jsonParser JsonParser
marshalErrorString string
unmarshalErrorString string
}
func (m *mockJsonParser) Marshal(v interface{}) ([]byte, error) {
if m.marshalErrorString != "" {
return nil, fmt.Errorf(m.marshalErrorString)
}
return m.jsonParser.Marshal(v)
}
func (m *mockJsonParser) Unmarshal(data []byte, v interface{}) error {
if m.unmarshalErrorString != "" {
return fmt.Errorf(m.unmarshalErrorString)
}
return m.jsonParser.Unmarshal(data, v)
}
type mockAuth0Credentials struct {
jwtToken JWTToken
err error
}
func (mc *mockAuth0Credentials) Authenticate() (JWTToken, error) {
return mc.jwtToken, mc.err
}
func newTestJWT(t *testing.T, expInt int) string {
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iat": now.Unix(),
"exp": now.Add(time.Duration(expInt) * time.Second).Unix(),
})
var hmacSampleSecret []byte
tokenString, err := token.SignedString(hmacSampleSecret)
if err != nil {
t.Fatal(err)
}
return tokenString
}
func TestAuth0_RequestJWTToken(t *testing.T) {
type requestJWTTokenTest struct {
name string
inputCode int
inputResBody string
helper ManagerHelper
expectedFuncExitErrDiff error
expectedCode int
expectedToken string
}
exp := 5
token := newTestJWT(t, exp)
requestJWTTokenTesttCase1 := requestJWTTokenTest{
name: "Get Good JWT Response",
inputCode: 200,
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
helper: JsonParser{},
expectedCode: 200,
expectedToken: token,
}
requestJWTTokenTestCase2 := requestJWTTokenTest{
name: "Request Bad Status Code",
inputCode: 400,
inputResBody: "{}",
helper: JsonParser{},
expectedFuncExitErrDiff: fmt.Errorf("unable to get token, statusCode 400"),
expectedCode: 200,
expectedToken: "",
}
for _, testCase := range []requestJWTTokenTest{requestJWTTokenTesttCase1, requestJWTTokenTestCase2} {
t.Run(testCase.name, func(t *testing.T) {
jwtReqClient := mockHTTPClient{
resBody: testCase.inputResBody,
code: testCase.inputCode,
}
config := Auth0ClientConfig{}
creds := Auth0Credentials{
clientConfig: config,
httpClient: &jwtReqClient,
helper: testCase.helper,
}
res, err := creds.requestJWTToken()
if err != nil {
if testCase.expectedFuncExitErrDiff != nil {
assert.EqualError(t, err, testCase.expectedFuncExitErrDiff.Error(), "errors should be the same")
} else {
t.Fatal(err)
}
}
body, err := ioutil.ReadAll(res.Body)
assert.NoError(t, err, "unable to read the response body")
jwtToken := JWTToken{}
err = json.Unmarshal(body, &jwtToken)
assert.NoError(t, err, "unable to parse the json input")
assert.Equalf(t, testCase.expectedToken, jwtToken.AccessToken, "two tokens should be the same")
})
}
}
func TestAuth0_ParseRequestJWTResponse(t *testing.T) {
type parseRequestJWTResponseTest struct {
name string
inputResBody string
helper ManagerHelper
expectedToken string
expectedExpiresIn int
assertErrFunc func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool
assertErrFuncMessage string
}
exp := 100
token := newTestJWT(t, exp)
parseRequestJWTResponseTestCase1 := parseRequestJWTResponseTest{
name: "Parse Good JWT Body",
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
helper: JsonParser{},
expectedToken: token,
expectedExpiresIn: exp,
assertErrFunc: assert.NoError,
assertErrFuncMessage: "no error was expected",
}
parseRequestJWTResponseTestCase2 := parseRequestJWTResponseTest{
name: "Parse Bad json JWT Body",
inputResBody: "",
helper: JsonParser{},
expectedToken: "",
expectedExpiresIn: 0,
assertErrFunc: assert.Error,
assertErrFuncMessage: "json error was expected",
}
for _, testCase := range []parseRequestJWTResponseTest{parseRequestJWTResponseTestCase1, parseRequestJWTResponseTestCase2} {
t.Run(testCase.name, func(t *testing.T) {
rawBody := ioutil.NopCloser(strings.NewReader(testCase.inputResBody))
config := Auth0ClientConfig{}
creds := Auth0Credentials{
clientConfig: config,
helper: testCase.helper,
}
jwtToken, err := creds.parseRequestJWTResponse(rawBody)
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
assert.Equalf(t, testCase.expectedToken, jwtToken.AccessToken, "two tokens should be the same")
assert.Equalf(t, testCase.expectedExpiresIn, jwtToken.ExpiresIn, "the two expire times should be the same")
})
}
}
func TestAuth0_JwtStillValid(t *testing.T) {
type jwtStillValidTest struct {
name string
inputTime time.Time
expectedResult bool
message string
}
jwtStillValidTestCase1 := jwtStillValidTest{
name: "JWT still valid",
inputTime: time.Now().Add(10 * time.Second),
expectedResult: true,
message: "should be true",
}
jwtStillValidTestCase2 := jwtStillValidTest{
name: "JWT is invalid",
inputTime: time.Now(),
expectedResult: false,
message: "should be false",
}
for _, testCase := range []jwtStillValidTest{jwtStillValidTestCase1, jwtStillValidTestCase2} {
t.Run(testCase.name, func(t *testing.T) {
config := Auth0ClientConfig{}
creds := Auth0Credentials{
clientConfig: config,
}
creds.jwtToken.expiresInTime = testCase.inputTime
assert.Equalf(t, testCase.expectedResult, creds.jwtStillValid(), testCase.message)
})
}
}
func TestAuth0_Authenticate(t *testing.T) {
type authenticateTest struct {
name string
inputCode int
inputResBody string
inputExpireToken time.Time
helper ManagerHelper
expectedFuncExitErrDiff error
expectedCode int
expectedToken string
}
exp := 5
token := newTestJWT(t, exp)
authenticateTestCase1 := authenticateTest{
name: "Get Cached token",
inputExpireToken: time.Now().Add(30 * time.Second),
helper: JsonParser{},
//expectedFuncExitErrDiff: fmt.Errorf("unable to get token, statusCode 400"),
expectedCode: 200,
expectedToken: "",
}
authenticateTestCase2 := authenticateTest{
name: "Get Good JWT Response",
inputCode: 200,
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
helper: JsonParser{},
expectedCode: 200,
expectedToken: token,
}
authenticateTestCase3 := authenticateTest{
name: "Get Bad Status Code",
inputCode: 400,
inputResBody: "{}",
helper: JsonParser{},
expectedFuncExitErrDiff: fmt.Errorf("unable to get token, statusCode 400"),
expectedCode: 200,
expectedToken: "",
}
for _, testCase := range []authenticateTest{authenticateTestCase1, authenticateTestCase2, authenticateTestCase3} {
t.Run(testCase.name, func(t *testing.T) {
jwtReqClient := mockHTTPClient{
resBody: testCase.inputResBody,
code: testCase.inputCode,
}
config := Auth0ClientConfig{}
creds := Auth0Credentials{
clientConfig: config,
httpClient: &jwtReqClient,
helper: testCase.helper,
}
creds.jwtToken.expiresInTime = testCase.inputExpireToken
_, err := creds.Authenticate()
if err != nil {
if testCase.expectedFuncExitErrDiff != nil {
assert.EqualError(t, err, testCase.expectedFuncExitErrDiff.Error(), "errors should be the same")
} else {
t.Fatal(err)
}
}
assert.Equalf(t, testCase.expectedToken, creds.jwtToken.AccessToken, "two tokens should be the same")
})
}
}
func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
type updateUserAppMetadataTest struct {
name string
inputReqBody string
expectedReqBody string
appMetadata AppMetadata
statusCode int
helper ManagerHelper
managerCreds ManagerCredentials
assertErrFunc func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool
assertErrFuncMessage string
}
exp := 15
token := newTestJWT(t, exp)
appMetadata := AppMetadata{WTAccountId: "ok"}
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
name: "Bad Authentication",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: "",
appMetadata: appMetadata,
statusCode: 400,
helper: JsonParser{},
managerCreds: &mockAuth0Credentials{
jwtToken: JWTToken{},
err: fmt.Errorf("error"),
},
assertErrFunc: assert.Error,
assertErrFuncMessage: "should return error",
}
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
name: "Bad Status Code",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: fmt.Sprintf("{\"app_metadata\": {\"wt_account_id\":\"%s\"}}", appMetadata.WTAccountId),
appMetadata: appMetadata,
statusCode: 400,
helper: JsonParser{},
managerCreds: &mockAuth0Credentials{
jwtToken: JWTToken{},
},
assertErrFunc: assert.Error,
assertErrFuncMessage: "should return error",
}
updateUserAppMetadataTestCase3 := updateUserAppMetadataTest{
name: "Bad Response Parsing",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
statusCode: 400,
helper: &mockJsonParser{marshalErrorString: "error"},
assertErrFunc: assert.Error,
assertErrFuncMessage: "should return error",
}
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
name: "Good request",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: fmt.Sprintf("{\"app_metadata\": {\"wt_account_id\":\"%s\"}}", appMetadata.WTAccountId),
appMetadata: appMetadata,
statusCode: 200,
helper: JsonParser{},
assertErrFunc: assert.NoError,
assertErrFuncMessage: "shouldn't return error",
}
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2, updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4} {
t.Run(testCase.name, func(t *testing.T) {
jwtReqClient := mockHTTPClient{
resBody: testCase.inputReqBody,
code: testCase.statusCode,
}
config := Auth0ClientConfig{}
var creds ManagerCredentials
if testCase.managerCreds != nil {
creds = testCase.managerCreds
} else {
creds = &Auth0Credentials{
clientConfig: config,
httpClient: &jwtReqClient,
helper: testCase.helper,
}
}
manager := Auth0Manager{
httpClient: &jwtReqClient,
credentials: creds,
helper: testCase.helper,
}
err := manager.UpdateUserAppMetadata("1", testCase.appMetadata)
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
assert.Equal(t, testCase.expectedReqBody, jwtReqClient.reqBody, "request body should match")
})
}
}

View File

@ -0,0 +1,63 @@
package idp
import (
"fmt"
"net/http"
"strings"
"time"
)
// Manager idp manager interface
type Manager interface {
UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error
}
// Config an idp configuration struct to be loaded from management server's config file
type Config struct {
ManagerType string
Auth0ClientCredentials Auth0ClientConfig
}
// ManagerCredentials interface that authenticates using the credential of each type of idp
type ManagerCredentials interface {
Authenticate() (JWTToken, error)
}
// ManagerHTTPClient http client interface for API calls
type ManagerHTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// ManagerHelper helper
type ManagerHelper interface {
Marshal(v interface{}) ([]byte, error)
Unmarshal(data []byte, v interface{}) error
}
// AppMetadata user app metadata to associate with a profile
type AppMetadata struct {
// Wiretrustee account id to update in the IDP
// maps to wt_account_id when json.marshal
WTAccountId string `json:"wt_account_id"`
}
// JWTToken a JWT object that holds information of a token
type JWTToken struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
expiresInTime time.Time
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}
// NewManager returns a new idp manager based on the configuration that it receives
func NewManager(config Config) (Manager, error) {
switch strings.ToLower(config.ManagerType) {
case "none", "":
return nil, nil
case "auth0":
return NewAuth0Manager(config.Auth0ClientCredentials), nil
default:
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
}
}

View File

@ -0,0 +1,13 @@
package idp
import "encoding/json"
type JsonParser struct{}
func (JsonParser) Marshal(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func (JsonParser) Unmarshal(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}

View File

@ -313,7 +313,7 @@ func startManagement(port int, config *Config) (*grpc.Server, error) {
return nil, err
}
peersUpdateManager := NewPeersUpdateManager()
accountManager := NewManager(store, peersUpdateManager)
accountManager := NewManager(store, peersUpdateManager, nil)
turnManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {

View File

@ -496,7 +496,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()
accountManager := server.NewManager(store, peersUpdateManager)
accountManager := server.NewManager(store, peersUpdateManager, nil)
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
Expect(err).NotTo(HaveOccurred())

View File

@ -33,5 +33,15 @@
"AuthIssuer": "<PASTE YOUR AUTH0 ISSUER HERE>,",
"AuthAudience": "<PASTE YOUR AUTH0 AUDIENCE HERE>",
"AuthKeysLocation": "<PASTE YOUR AUTH0 PUBLIC JWT KEYS LOCATION HERE>"
},
"IdpManagerConfig": {
"Manager": "<none|auth0>",
"Auth0ClientCredentials": {
"Audience": "<PASTE YOUR AUTH0 AUDIENCE HERE>",
"AuthIssuer": "<PASTE YOUR AUTH0 Auth Issuer HERE>",
"ClientId": "<PASTE YOUR AUTH0 Application Client ID HERE>",
"ClientSecret": "<PASTE YOUR AUTH0 Application Client Secret HERE>",
"GrantType": "client_credentials"
}
}
}