Merge pull request #190 from fastenhealth/epic_dynamic_client_registration
This commit is contained in:
commit
ae4903ec22
|
@ -36,6 +36,7 @@ type DatabaseRepository interface {
|
|||
GetSource(context.Context, string) (*models.SourceCredential, error)
|
||||
GetSourceSummary(context.Context, string) (*models.SourceSummary, error)
|
||||
GetSources(context.Context) ([]models.SourceCredential, error)
|
||||
UpdateSource(ctx context.Context, sourceCreds *models.SourceCredential) error
|
||||
|
||||
CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error
|
||||
GetGlossaryEntry(ctx context.Context, code string, codeSystem string) (*models.Glossary, error)
|
||||
|
|
|
@ -359,6 +359,20 @@ func (mr *MockDatabaseRepositoryMockRecorder) RemoveResourceAssociation(ctx, sou
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveResourceAssociation", reflect.TypeOf((*MockDatabaseRepository)(nil).RemoveResourceAssociation), ctx, source, resourceType, resourceId, relatedSource, relatedResourceType, relatedResourceId)
|
||||
}
|
||||
|
||||
// UpdateSource mocks base method.
|
||||
func (m *MockDatabaseRepository) UpdateSource(ctx context.Context, sourceCreds *models0.SourceCredential) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateSource", ctx, sourceCreds)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateSource indicates an expected call of UpdateSource.
|
||||
func (mr *MockDatabaseRepositoryMockRecorder) UpdateSource(ctx, sourceCreds interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSource", reflect.TypeOf((*MockDatabaseRepository)(nil).UpdateSource), ctx, sourceCreds)
|
||||
}
|
||||
|
||||
// UpsertRawResource mocks base method.
|
||||
func (m *MockDatabaseRepository) UpsertRawResource(ctx context.Context, sourceCredentials models.SourceCredential, rawResource models.RawResourceFhir) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -775,6 +775,29 @@ func (sr *SqliteRepository) CreateSource(ctx context.Context, sourceCreds *model
|
|||
Assign(*sourceCreds).FirstOrCreate(sourceCreds).Error
|
||||
}
|
||||
|
||||
func (sr *SqliteRepository) UpdateSource(ctx context.Context, sourceCreds *models.SourceCredential) error {
|
||||
currentUser, currentUserErr := sr.GetCurrentUser(ctx)
|
||||
if currentUserErr != nil {
|
||||
return currentUserErr
|
||||
}
|
||||
sourceCreds.UserID = currentUser.ID
|
||||
|
||||
//Assign will **always** update the source credential in the DB with data passed into this function.
|
||||
return sr.GormClient.WithContext(ctx).
|
||||
Where(models.SourceCredential{
|
||||
ModelBase: models.ModelBase{ID: sourceCreds.ID},
|
||||
UserID: sourceCreds.UserID,
|
||||
SourceType: sourceCreds.SourceType,
|
||||
}).Updates(models.SourceCredential{
|
||||
AccessToken: sourceCreds.AccessToken,
|
||||
RefreshToken: sourceCreds.RefreshToken,
|
||||
ExpiresAt: sourceCreds.ExpiresAt,
|
||||
DynamicClientId: sourceCreds.DynamicClientId,
|
||||
DynamicClientRegistrationMode: sourceCreds.DynamicClientRegistrationMode,
|
||||
DynamicClientJWKS: sourceCreds.DynamicClientJWKS,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (sr *SqliteRepository) GetSource(ctx context.Context, sourceId string) (*models.SourceCredential, error) {
|
||||
currentUser, currentUserErr := sr.GetCurrentUser(ctx)
|
||||
if currentUserErr != nil {
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package jwk
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
)
|
||||
|
||||
//see https://github.com/lestrrat-go/jwx/blob/v2/docs/04-jwk.md#working-with-key-specific-methods
|
||||
func JWKGenerate() (jwk.RSAPrivateKey, error) {
|
||||
raw, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate RSA private key: %s\n", err)
|
||||
}
|
||||
|
||||
key, err := jwk.FromRaw(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create jwk.Key from RSA private key: %s\n", err)
|
||||
}
|
||||
|
||||
rsakey, ok := key.(jwk.RSAPrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert jwk.Key into jwk.RSAPrivateKey (was %T)\n", key)
|
||||
}
|
||||
rsakey.Set("kid", uuid.New().String())
|
||||
|
||||
//_ = rsakey.D()
|
||||
//_ = rsakey.DP()
|
||||
//_ = rsakey.DQ()
|
||||
//_ = rsakey.E()
|
||||
//_ = rsakey.N()
|
||||
//_ = rsakey.P()
|
||||
//_ = rsakey.Q()
|
||||
//_ = rsakey.QI()
|
||||
//// OUTPUT:
|
||||
|
||||
return rsakey, nil
|
||||
}
|
||||
|
||||
func JWKSerialize(privateKey jwk.RSAPrivateKey) (map[string]string, error) {
|
||||
jsonbuf, err := json.Marshal(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var dict map[string]string
|
||||
err = json.Unmarshal(jsonbuf, &dict)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if privateKey.KeyID() != "" {
|
||||
dict["kid"] = privateKey.KeyID()
|
||||
}
|
||||
|
||||
return dict, err
|
||||
}
|
||||
|
||||
func JWKDeserialize(privateKeyDict map[string]string) (jwk.RSAPrivateKey, error) {
|
||||
jsonbuf, err := json.Marshal(privateKeyDict)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := jwk.ParseKey(jsonbuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create jwk.Key from RSA private key: %s\n", err)
|
||||
}
|
||||
return key.(jwk.RSAPrivateKey), nil
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package jwk
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJWKSerialize(t *testing.T) {
|
||||
//test
|
||||
keypair, err := JWKGenerate()
|
||||
require.NoError(t, err)
|
||||
|
||||
dict, err := JWKSerialize(keypair)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := []string{}
|
||||
for key, _ := range dict {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
//assert
|
||||
require.NotEmpty(t, keypair.KeyID())
|
||||
require.ElementsMatch(t, []string{"d", "dp", "dq", "e", "kty", "n", "p", "q", "qi", "kid"}, keys)
|
||||
require.Equal(t, "RSA", dict["kty"])
|
||||
}
|
||||
|
||||
func TestJWKDeserialize(t *testing.T) {
|
||||
//setup
|
||||
keypairDict := map[string]string{
|
||||
"kty": "RSA",
|
||||
"kid": "cc34c0a0-bd5a-4a3c-a50d-a2a7db7643df",
|
||||
"n": "pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv_hSpm8T8EtBDxrUdi1OHZfMhUixGaut-3nQ4GG9nM249oxhCtxqqNvEXrmQRGqczyLxuh-fKn9Fg--hS9UpazHpfVAFnB5aCfXoNhPuI8oByyFKMKaOVgHNqP5NBEqabiLftZD3W_lsFCPGuzr4Vp0YS7zS2hDYScC2oOMu4rGU1LcMZf39p3153Cq7bS2Xh6Y-vw5pwzFYZdjQxDn8x8BG3fJ6j8TGLXQsbKH1218_HcUJRvMwdpbUQG5nvA2GXVqLqdwp054Lzk9_B_f1lVrmOKuHjTNHq48w",
|
||||
"e": "AQAB",
|
||||
"d": "ksDmucdMJXkFGZxiomNHnroOZxe8AmDLDGO1vhs-POa5PZM7mtUPonxwjVmthmpbZzla-kg55OFfO7YcXhg-Hm2OWTKwm73_rLh3JavaHjvBqsVKuorX3V3RYkSro6HyYIzFJ1Ek7sLxbjDRcDOj4ievSX0oN9l-JZhaDYlPlci5uJsoqro_YrE0PRRWVhtGynd-_aWgQv1YzkfZuMD-hJtDi1Im2humOWxA4eZrFs9eG-whXcOvaSwO4sSGbS99ecQZHM2TcdXeAs1PvjVgQ_dKnZlGN3lTWoWfQP55Z7Tgt8Nf1q4ZAKd-NlMe-7iqCFfsnFwXjSiaOa2CRGZn-Q",
|
||||
"p": "4A5nU4ahEww7B65yuzmGeCUUi8ikWzv1C81pSyUKvKzu8CX41hp9J6oRaLGesKImYiuVQK47FhZ--wwfpRwHvSxtNU9qXb8ewo-BvadyO1eVrIk4tNV543QlSe7pQAoJGkxCia5rfznAE3InKF4JvIlchyqs0RQ8wx7lULqwnn0",
|
||||
"q": "ven83GM6SfrmO-TBHbjTk6JhP_3CMsIvmSdo4KrbQNvp4vHO3w1_0zJ3URkmkYGhz2tgPlfd7v1l2I6QkIh4Bumdj6FyFZEBpxjE4MpfdNVcNINvVj87cLyTRmIcaGxmfylY7QErP8GFA-k4UoH_eQmGKGK44TRzYj5hZYGWIC8",
|
||||
"dp": "lmmU_AG5SGxBhJqb8wxfNXDPJjf__i92BgJT2Vp4pskBbr5PGoyV0HbfUQVMnw977RONEurkR6O6gxZUeCclGt4kQlGZ-m0_XSWx13v9t9DIbheAtgVJ2mQyVDvK4m7aRYlEceFh0PsX8vYDS5o1txgPwb3oXkPTtrmbAGMUBpE",
|
||||
"dq": "mxRTU3QDyR2EnCv0Nl0TCF90oliJGAHR9HJmBe__EjuCBbwHfcT8OG3hWOv8vpzokQPRl5cQt3NckzX3fs6xlJN4Ai2Hh2zduKFVQ2p-AF2p6Yfahscjtq-GY9cB85NxLy2IXCC0PF--Sq9LOrTE9QV988SJy_yUrAjcZ5MmECk",
|
||||
"qi": "ldHXIrEmMZVaNwGzDF9WG8sHj2mOZmQpw9yrjLK9hAsmsNr5LTyqWAqJIYZSwPTYWhY4nu2O0EY9G9uYiqewXfCKw_UngrJt8Xwfq1Zruz0YY869zPN4GiE9-9rzdZB33RBw8kIOquY3MK74FMwCihYx_LiU2YTHkaoJ3ncvtvg",
|
||||
}
|
||||
|
||||
//test
|
||||
keypair, err := JWKDeserialize(keypairDict)
|
||||
require.NoError(t, err)
|
||||
|
||||
serialized, err := JWKSerialize(keypair)
|
||||
require.NoError(t, err)
|
||||
|
||||
//assert
|
||||
require.NotEmpty(t, keypair.KeyID())
|
||||
require.Equal(t, keypair.KeyID(), "cc34c0a0-bd5a-4a3c-a50d-a2a7db7643df")
|
||||
require.Equal(t, keypair.KeyType(), jwa.KeyType("RSA"))
|
||||
|
||||
require.Equal(t, keypairDict, serialized)
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package models
|
||||
|
||||
type ClientRegistrationRequest struct {
|
||||
SoftwareId string `json:"software_id"`
|
||||
Jwks ClientRegistrationRequestJwks `json:"jwks"`
|
||||
}
|
||||
|
||||
type ClientRegistrationRequestJwks struct {
|
||||
Keys []ClientRegistrationRequestJwksKey `json:"keys"`
|
||||
}
|
||||
|
||||
type ClientRegistrationRequestJwksKey struct {
|
||||
KeyId string `json:"kid"`
|
||||
KeyType string `json:"kty"`
|
||||
PublicExponent string `json:"e"`
|
||||
Modulus string `json:"n"`
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package models
|
||||
|
||||
// ClientRegistrationResponse {
|
||||
// "redirect_uris": [
|
||||
// " https://fhir.epic.com/test/smart"
|
||||
// ],
|
||||
// "token_endpoint_auth_method": "none",
|
||||
// "grant_types": [
|
||||
// "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
||||
// ],
|
||||
// "software_id": " d45049c3-3441-40ef-ab4d-b9cd86a17225",
|
||||
// "client_id": "G65DA2AF4-1C91-11EC-9280-0050568B7514",
|
||||
// "client_id_issued_at": 1632417134,
|
||||
// "jwks": {
|
||||
// "keys": [{
|
||||
// "kty": "RSA",
|
||||
// "n": "vGASMnWdI-ManPgJi5XeT15Uf1tgpaNBmxfa-_bKG6G1DDTsYBy2K1uubppWMcl8Ff_2oWe6wKDMx2-bvrQQkR1zcV96yOgNmfDXuSSR1y7xk1Kd-uUhvmIKk81UvKbKOnPetnO1IftpEBm5Llzy-1dN3kkJqFabFSd3ujqi2ZGuvxfouZ-S3lpTU3O6zxNR6oZEbP2BwECoBORL5cOWOu_pYJvALf0njmamRQ2FKKCC-pf0LBtACU9tbPgHorD3iDdis1_cvk16i9a3HE2h4Hei4-nDQRXfVgXLzgr7GdJf1ArR1y65LVWvtuwNf7BaxVkEae1qKVLa2RUeg8imuw",
|
||||
// "e": "AQAB"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//}
|
||||
type ClientRegistrationResponse struct {
|
||||
RedirectUrls []string `json:"redirect_uris"`
|
||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
SoftwareId string `json:"software_id"`
|
||||
ClientId string `json:"client_id"`
|
||||
ClientIdIssuedAt int `json:"client_id_issued_at"`
|
||||
Jwks ClientRegistrationRequestJwks `json:"jwks"`
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package models
|
||||
|
||||
type ClientRegistrationTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
State string `json:"state"`
|
||||
Patient string `json:"patient"`
|
||||
EpicDstu2Patient string `json:"__epic.dstu2.patient"`
|
||||
}
|
|
@ -1,8 +1,18 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fasten-sources/pkg"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/jwk"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SourceCredential Data/Medical Provider Credentials
|
||||
|
@ -18,6 +28,7 @@ type SourceCredential struct {
|
|||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
IntrospectionEndpoint string `json:"introspection_endpoint"`
|
||||
RegistrationEndpoint string `json:"registration_endpoint"` //optional - required when Dynamic Client Registration mode is set
|
||||
|
||||
Scopes []string `json:"scopes_supported" gorm:"type:text;serializer:json"`
|
||||
Issuer string `json:"issuer"`
|
||||
|
@ -33,8 +44,9 @@ type SourceCredential struct {
|
|||
ClientId string `json:"client_id"`
|
||||
RedirectUri string `json:"redirect_uri"` //lighthouse url the provider will redirect to (registered with App)
|
||||
|
||||
Confidential bool `json:"confidential"` //if enabled, requires client_secret to authenticate with provider (PKCE)
|
||||
CORSRelayRequired bool `json:"cors_relay_required"` //if true, requires CORS proxy/relay, as provider does not return proper response to CORS preflight
|
||||
Confidential bool `json:"confidential"` //if enabled, requires client_secret to authenticate with provider (PKCE)
|
||||
DynamicClientRegistrationMode string `json:"dynamic_client_registration_mode"` //if enabled, will dynamically register client with provider (https://oauth.net/2/dynamic-client-registration/)
|
||||
CORSRelayRequired bool `json:"cors_relay_required"` //if true, requires CORS proxy/relay, as provider does not return proper response to CORS preflight
|
||||
//SecretKeyPrefix string `json:"-"` //the secret key prefix to use, if empty (default) will use the sourceType value
|
||||
|
||||
// auth/credential data
|
||||
|
@ -44,6 +56,10 @@ type SourceCredential struct {
|
|||
ExpiresAt int64 `json:"expires_at"`
|
||||
CodeChallenge string `json:"code_challenge"`
|
||||
CodeVerifier string `json:"code_verifier"`
|
||||
|
||||
//dynamic client auth/credential data
|
||||
DynamicClientJWKS []map[string]string `json:"dynamic_client_jwks" gorm:"type:text;serializer:json"`
|
||||
DynamicClientId string `json:"dynamic_client_id"`
|
||||
}
|
||||
|
||||
func (s *SourceCredential) GetSourceType() pkg.SourceType {
|
||||
|
@ -82,7 +98,7 @@ func (s *SourceCredential) GetExpiresAt() int64 {
|
|||
return s.ExpiresAt
|
||||
}
|
||||
|
||||
func (s *SourceCredential) RefreshTokens(accessToken string, refreshToken string, expiresAt int64) {
|
||||
func (s *SourceCredential) SetTokens(accessToken string, refreshToken string, expiresAt int64) {
|
||||
if expiresAt > 0 && expiresAt != s.ExpiresAt {
|
||||
s.ExpiresAt = expiresAt
|
||||
}
|
||||
|
@ -108,5 +124,83 @@ tokenResponse: payload,
|
|||
expiresAt: getAccessTokenExpiration(payload, new BrowserAdapter()),
|
||||
codeChallenge: codeChallenge,
|
||||
codeVerifier: codeVerifier
|
||||
|
||||
*/
|
||||
|
||||
// IsDynamicClient this method is used to check if this source uses dynamic client registration (used to customize token refresh logic)
|
||||
func (s *SourceCredential) IsDynamicClient() bool {
|
||||
return len(s.DynamicClientRegistrationMode) > 0
|
||||
}
|
||||
|
||||
//this will set/update the AccessToken and Expiry using the dynamic client credentials
|
||||
func (s *SourceCredential) RefreshDynamicClientAccessToken() error {
|
||||
if len(s.DynamicClientRegistrationMode) == 0 {
|
||||
return fmt.Errorf("dynamic client registration mode not set")
|
||||
}
|
||||
if len(s.DynamicClientJWKS) == 0 {
|
||||
return fmt.Errorf("dynamic client jwks not set")
|
||||
}
|
||||
if len(s.DynamicClientId) == 0 {
|
||||
return fmt.Errorf("dynamic client id not set")
|
||||
}
|
||||
|
||||
//convert the serialized dynamic-client credentials to a jwx.Key
|
||||
jwkeypair, err := jwk.JWKDeserialize(s.DynamicClientJWKS[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// see https://github.com/lestrrat-go/jwx/tree/v2/jwt#token-usage
|
||||
t := jwt.New()
|
||||
t.Set("kid", jwkeypair.KeyID())
|
||||
t.Set(jwt.SubjectKey, s.DynamicClientId)
|
||||
t.Set(jwt.AudienceKey, s.TokenEndpoint)
|
||||
t.Set(jwt.JwtIDKey, uuid.New().String())
|
||||
t.Set(jwt.ExpirationKey, time.Now().Add(time.Minute*2).Unix()) // must be less than 5 minutes from now. Time when this JWT expires
|
||||
t.Set(jwt.IssuedAtKey, time.Now().Unix())
|
||||
t.Set(jwt.IssuerKey, s.DynamicClientId)
|
||||
|
||||
//sign the jwt with the private key
|
||||
// Signing a token (using raw rsa.PrivateKey)
|
||||
signed, err := jwt.Sign(t, jwt.WithKey(jwa.RS256, jwkeypair))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign dynamic-client jwt: %s", err)
|
||||
}
|
||||
|
||||
//send this signed jwt to the token endpoint to get a new access token
|
||||
// https://fhir.epic.com/Documentation?docId=oauth2§ion=JWKS
|
||||
|
||||
postForm := url.Values{
|
||||
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
|
||||
"assertion": {string(signed)},
|
||||
"client_id": {s.DynamicClientId},
|
||||
}
|
||||
|
||||
tokenResp, err := http.PostForm(s.TokenEndpoint, postForm)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while sending dynamic client token request, %s", err)
|
||||
}
|
||||
|
||||
defer tokenResp.Body.Close()
|
||||
if tokenResp.StatusCode >= 300 || tokenResp.StatusCode < 200 {
|
||||
|
||||
b, err := io.ReadAll(tokenResp.Body)
|
||||
if err == nil {
|
||||
log.Printf("Error Response body: %s", string(b))
|
||||
}
|
||||
|
||||
return fmt.Errorf("an error occurred while reading dynamic client token response, status code was not 200: %d", tokenResp.StatusCode)
|
||||
}
|
||||
|
||||
var registrationTokenResponseBytes ClientRegistrationTokenResponse
|
||||
err = json.NewDecoder(tokenResp.Body).Decode(®istrationTokenResponseBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while parsing dynamic client token response: %v", err)
|
||||
}
|
||||
|
||||
//update the source credential with the new access token
|
||||
s.AccessToken = registrationTokenResponseBytes.AccessToken
|
||||
s.ExpiresAt = time.Now().Add(time.Second * time.Duration(registrationTokenResponseBytes.ExpiresIn)).Unix()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fasten-sources/clients/factory"
|
||||
sourceModels "github.com/fastenhealth/fasten-sources/clients/models"
|
||||
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/jwk"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
@ -28,6 +32,108 @@ func CreateSource(c *gin.Context) {
|
|||
|
||||
logger.Infof("Parsed Create SourceCredential Credentials Payload: %v", sourceCred)
|
||||
|
||||
if sourceCred.DynamicClientRegistrationMode == "user-authenticated" {
|
||||
logger.Warnf("This client requires a dynamice client registration, starting registration process")
|
||||
|
||||
if len(sourceCred.RegistrationEndpoint) == 0 {
|
||||
logger.Errorln("Empty registration endpoint, cannot be used with dynamic-client registration mode:", sourceCred.DynamicClientRegistrationMode)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
//this source requires dynamic client registration
|
||||
// see https://fhir.epic.com/Documentation?docId=Oauth2§ion=Standalone-Oauth2-OfflineAccess-0
|
||||
|
||||
// Generate a public-private key pair
|
||||
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
|
||||
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while generating device-specific keypair for dynamic client", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
//store in sourceCredential
|
||||
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while serializing keypair for dynamic client", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
sourceCred.DynamicClientJWKS = []map[string]string{
|
||||
serializedKeypair,
|
||||
}
|
||||
|
||||
//generate dynamic client registration request
|
||||
payload := models.ClientRegistrationRequest{
|
||||
SoftwareId: sourceCred.ClientId,
|
||||
Jwks: models.ClientRegistrationRequestJwks{
|
||||
Keys: []models.ClientRegistrationRequestJwksKey{
|
||||
{
|
||||
KeyType: "RSA",
|
||||
KeyId: serializedKeypair["kid"],
|
||||
Modulus: serializedKeypair["n"],
|
||||
PublicExponent: serializedKeypair["e"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while marshalling dynamic client registration request", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
|
||||
req, err := http.NewRequest(http.MethodPost, sourceCred.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while generating dynamic client registration request", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sourceCred.AccessToken))
|
||||
|
||||
registrationResponse, err := http.DefaultClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while sending dynamic client registration request", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
defer registrationResponse.Body.Close()
|
||||
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
|
||||
logger.Errorln("An error occurred while reading dynamic client registration response, status code was not 200", registrationResponse.StatusCode)
|
||||
b, err := io.ReadAll(registrationResponse.Body)
|
||||
if err == nil {
|
||||
logger.Printf("Error Response body: %s", string(b))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
//read response
|
||||
var registrationResponseBytes models.ClientRegistrationResponse
|
||||
err = json.NewDecoder(registrationResponse.Body).Decode(®istrationResponseBytes)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while parsing dynamic client registration response", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
//store the dynamic client id
|
||||
sourceCred.DynamicClientId = registrationResponseBytes.ClientId
|
||||
|
||||
//generate a JWT token and then use it to get an access token for the dynamic client
|
||||
err = sourceCred.RefreshDynamicClientAccessToken()
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while retrieving access token for dynamic client", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := databaseRepo.CreateSource(c, &sourceCred)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while storing source credential", err)
|
||||
|
@ -185,20 +291,25 @@ func SyncSourceResources(c context.Context, logger *logrus.Entry, databaseRepo d
|
|||
logger.Errorln("An error occurred while initializing hub client using source credential", err)
|
||||
return sourceModels.UpsertSummary{}, err
|
||||
}
|
||||
//TODO: update source
|
||||
//if updatedSource != nil {
|
||||
// logger.Warnf("TODO: source credential has been updated, we should store it in the database: %v", updatedSource)
|
||||
// //err := databaseRepo.CreateSource(c, updatedSource)
|
||||
// //if err != nil {
|
||||
// // logger.Errorln("An error occurred while updating source credential", err)
|
||||
// // return err
|
||||
// //}
|
||||
//}
|
||||
|
||||
summary, err := sourceClient.SyncAll(databaseRepo)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while bulk import of resources from source", err)
|
||||
return summary, err
|
||||
}
|
||||
|
||||
//update source incase the access token/refresh token has been updated
|
||||
sourceCredential := sourceClient.GetSourceCredential()
|
||||
sourceCredentialConcrete, ok := sourceCredential.(*models.SourceCredential)
|
||||
if !ok {
|
||||
logger.Errorln("An error occurred while updating source credential, source credential is not of type *models.SourceCredential")
|
||||
return summary, fmt.Errorf("source credential is not of type *models.SourceCredential")
|
||||
}
|
||||
err = databaseRepo.UpdateSource(c, sourceCredentialConcrete)
|
||||
if err != nil {
|
||||
logger.Errorf("An error occurred while updating source credential: %v", err)
|
||||
return summary, err
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
|
|
@ -141,6 +141,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
|||
token_endpoint: sourceMetadata.token_endpoint,
|
||||
introspection_endpoint: sourceMetadata.introspection_endpoint,
|
||||
userinfo_endpoint: sourceMetadata.userinfo_endpoint,
|
||||
registration_endpoint: sourceMetadata.registration_endpoint,
|
||||
api_endpoint_base_url: sourceMetadata.api_endpoint_base_url,
|
||||
client_id: sourceMetadata.client_id,
|
||||
redirect_uri: sourceMetadata.redirect_uri,
|
||||
|
@ -158,6 +159,8 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
|||
refresh_token: payload.refresh_token,
|
||||
id_token: payload.id_token,
|
||||
|
||||
dynamic_client_registration_mode: sourceMetadata.dynamic_client_registration_mode,
|
||||
|
||||
// @ts-ignore - in some cases the getAccessTokenExpiration is a string, which cases failures to store Source in db.
|
||||
expires_at: parseInt(this.getAccessTokenExpiration(payload)),
|
||||
})
|
||||
|
|
|
@ -11,6 +11,10 @@ export class Source extends LighthouseSourceMetadata{
|
|||
id_token?: string
|
||||
expires_at: number //seconds since epoch
|
||||
|
||||
|
||||
dynamic_client_jwks?: any[]
|
||||
dynamic_client_id?: string
|
||||
|
||||
constructor(object: any) {
|
||||
super()
|
||||
return Object.assign(this, object)
|
||||
|
|
|
@ -6,6 +6,7 @@ export class LighthouseSourceMetadata extends MetadataSource {
|
|||
token_endpoint: string
|
||||
introspection_endpoint: string
|
||||
userinfo_endpoint: string
|
||||
registration_endpoint: string
|
||||
|
||||
scopes_supported: string[]
|
||||
issuer: string
|
||||
|
@ -20,5 +21,6 @@ export class LighthouseSourceMetadata extends MetadataSource {
|
|||
redirect_uri: string
|
||||
|
||||
confidential: boolean
|
||||
dynamic_client_registration_mode: string
|
||||
cors_relay_required: boolean
|
||||
}
|
||||
|
|
14
go.mod
14
go.mod
|
@ -7,7 +7,7 @@ require (
|
|||
github.com/dave/jennifer v1.6.1
|
||||
github.com/dominikbraun/graph v0.15.0
|
||||
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3
|
||||
github.com/fastenhealth/fasten-sources v0.2.1
|
||||
github.com/fastenhealth/fasten-sources v0.2.3
|
||||
github.com/fastenhealth/gofhir-models v0.0.5
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/glebarez/sqlite v1.5.0
|
||||
|
@ -15,12 +15,13 @@ require (
|
|||
github.com/golang/mock v1.6.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.11
|
||||
github.com/philips-software/go-hsdp-api v0.81.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.11.2
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/crypto v0.9.0
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
|
||||
golang.org/x/net v0.10.0
|
||||
gorm.io/datatypes v1.0.7
|
||||
|
@ -34,6 +35,7 @@ require (
|
|||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
|
@ -57,6 +59,11 @@ require (
|
|||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
|
@ -73,6 +80,7 @@ require (
|
|||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/samber/lo v1.35.0 // indirect
|
||||
github.com/seborama/govcr v4.5.0+incompatible // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
|
|
37
go.sum
37
go.sum
|
@ -155,6 +155,9 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA=
|
||||
github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
|
@ -188,8 +191,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
|
|||
github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=
|
||||
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/fastenhealth/fasten-sources v0.2.1 h1:ZdQXg3cRelPo/WsguCU+Ic3F/4GfROeCj/LwIm9NpVM=
|
||||
github.com/fastenhealth/fasten-sources v0.2.1/go.mod h1:B7pVQcwLuL+rgjSHwlu3p0CySyHN262BkfbYMKVKXTk=
|
||||
github.com/fastenhealth/fasten-sources v0.2.3 h1:a40yp/cim0PUf6DFH3WgMW4a6J7cgUDycdQqur1dq0c=
|
||||
github.com/fastenhealth/fasten-sources v0.2.3/go.mod h1:B7pVQcwLuL+rgjSHwlu3p0CySyHN262BkfbYMKVKXTk=
|
||||
github.com/fastenhealth/gofhir-models v0.0.5 h1:wU2Dz+/h9MzZCTRgkQzeq5l0EFuMI6C5xgCbKislFpg=
|
||||
github.com/fastenhealth/gofhir-models v0.0.5/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
|
@ -527,6 +530,19 @@ github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 h1:3tLzEnUizyN9YLW
|
|||
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
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/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
|
@ -678,6 +694,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
|||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/seborama/govcr v4.5.0+incompatible h1:XvdHtXi0d4cUAn+0aWolvwfS3nmhNC8Z+yMQwn/M64I=
|
||||
github.com/seborama/govcr v4.5.0+incompatible/go.mod h1:EgcISudCCYDLzbiAImJ8i7kk4+wTA44Kp+j4S0LhASI=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE=
|
||||
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
|
@ -723,12 +741,14 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
|
||||
github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
|
||||
github.com/tchap/go-patricia v0.0.0-20160729071656-dd168db6051b/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
|
||||
|
@ -819,8 +839,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -860,6 +880,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -910,6 +931,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
|||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
@ -935,6 +957,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -1007,12 +1030,14 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1025,6 +1050,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -1098,6 +1124,7 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
Loading…
Reference in New Issue