diff --git a/backend/pkg/jwk/jwk_utils.go b/backend/pkg/jwk/jwk_utils.go new file mode 100644 index 000000000..7c7712a67 --- /dev/null +++ b/backend/pkg/jwk/jwk_utils.go @@ -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 +} diff --git a/backend/pkg/jwk/jwk_utils_test.go b/backend/pkg/jwk/jwk_utils_test.go new file mode 100644 index 000000000..3cc472a1c --- /dev/null +++ b/backend/pkg/jwk/jwk_utils_test.go @@ -0,0 +1,60 @@ +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"]) + //require.Equal(t, map[string]string{}, dict) + + require.Equal(t, string(keypair.N()), dict["n"]) +} + +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) + +} diff --git a/backend/pkg/models/client_registration_request.go b/backend/pkg/models/client_registration_request.go new file mode 100644 index 000000000..36a7ae622 --- /dev/null +++ b/backend/pkg/models/client_registration_request.go @@ -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"` +} diff --git a/backend/pkg/models/client_registration_response.go b/backend/pkg/models/client_registration_response.go new file mode 100644 index 000000000..124ac1792 --- /dev/null +++ b/backend/pkg/models/client_registration_response.go @@ -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"` +} diff --git a/backend/pkg/models/client_registration_token_response.go b/backend/pkg/models/client_registration_token_response.go new file mode 100644 index 000000000..932298dc5 --- /dev/null +++ b/backend/pkg/models/client_registration_token_response.go @@ -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"` +} diff --git a/backend/pkg/models/source_credential.go b/backend/pkg/models/source_credential.go index 660bc3724..0a9e8fe0e 100644 --- a/backend/pkg/models/source_credential.go +++ b/backend/pkg/models/source_credential.go @@ -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("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", 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 +} diff --git a/backend/pkg/web/handler/source.go b/backend/pkg/web/handler/source.go index 9d514787d..3227675c7 100644 --- a/backend/pkg/web/handler/source.go +++ b/backend/pkg/web/handler/source.go @@ -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("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) diff --git a/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.ts b/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.ts index 08657baf2..7c5e15d95 100644 --- a/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.ts +++ b/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.ts @@ -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)), }) diff --git a/frontend/src/app/models/fasten/source.ts b/frontend/src/app/models/fasten/source.ts index b6abf9849..576b0938f 100644 --- a/frontend/src/app/models/fasten/source.ts +++ b/frontend/src/app/models/fasten/source.ts @@ -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) diff --git a/frontend/src/app/models/lighthouse/lighthouse-source-metadata.ts b/frontend/src/app/models/lighthouse/lighthouse-source-metadata.ts index 7075fe627..60779de09 100644 --- a/frontend/src/app/models/lighthouse/lighthouse-source-metadata.ts +++ b/frontend/src/app/models/lighthouse/lighthouse-source-metadata.ts @@ -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 } diff --git a/go.mod b/go.mod index 838809bdf..c2dce3b31 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index a0f781cfc..18d112423 100644 --- a/go.sum +++ b/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=