From 551c8a1210a147588f54d012c746d2c1d89da9fd Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Mon, 18 Sep 2023 11:32:01 +0200 Subject: [PATCH] Add client metadata + API (#2488) * add known client metadata * pr feedback * revert error return --- auth/api/iam/api.go | 39 +++ auth/api/iam/api_test.go | 70 +++++ auth/api/iam/generated.go | 86 ++++++ .../metadata_types.go | 270 ++++++++++++++++++ auth/api/iam/metadata.go | 21 ++ auth/api/iam/metadata_test.go | 38 ++- auth/api/iam/types.go | 83 +++++- codegen/configs/auth_iam.yaml | 3 +- docs/_static/auth/iam.yaml | 39 ++- 9 files changed, 633 insertions(+), 16 deletions(-) diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 921e639a38..a59f44b9ea 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -22,6 +22,7 @@ import ( "context" "embed" "errors" + "fmt" "github.com/labstack/echo/v4" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" @@ -34,6 +35,7 @@ import ( vdr "github.com/nuts-foundation/nuts-node/vdr/types" "html/template" "net/http" + "strings" "sync" ) @@ -250,6 +252,43 @@ func (r Wrapper) GetWebDID(ctx context.Context, request GetWebDIDRequestObject) return GetWebDID200JSONResponse(*document), nil } +// GetOAuthClientMetadata returns the OAuth2 Client metadata for the request.Id if it is managed by this node. +func (r Wrapper) GetOAuthClientMetadata(ctx context.Context, request GetOAuthClientMetadataRequestObject) (GetOAuthClientMetadataResponseObject, error) { + if err := r.validateAsNutsFingerprint(ctx, request.Id); err != nil { + return nil, fmt.Errorf("client metadata: %w", err) + } + + identity := r.auth.PublicURL().JoinPath("iam", request.Id) + + return GetOAuthClientMetadata200JSONResponse(clientMetadata(*identity)), nil +} + +func (r Wrapper) validateAsNutsFingerprint(ctx context.Context, fingerprint string) error { + // convert fingerprint to did:nuts + if strings.HasPrefix(fingerprint, "did:") { + return core.InvalidInputError("id contains full did") + } + nutsDID, err := did.ParseDID("did:nuts:" + fingerprint) + if err != nil { + return core.InvalidInputError(err.Error()) + } + + // assert ownership of did + owned, err := r.vdr.IsOwner(ctx, *nutsDID) + if err != nil { + if didservice.IsFunctionalResolveError(err) { + return core.NotFoundError(err.Error()) + } + log.Logger().WithField("did", nutsDID.String()).Errorf("oauth metadata: failed to assert ownership of did: %s", err.Error()) + return core.Error(500, err.Error()) + } + if !owned { + return core.NotFoundError("did not owned") + } + + return nil +} + func createSession(params map[string]string, ownDID did.DID) *Session { session := &Session{ // TODO: Validate client ID diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 82057f1bab..17ff6bdb36 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -145,6 +145,76 @@ func TestWrapper_GetWebDID(t *testing.T) { }) } +func TestWrapper_GetOAuthClientMetadata(t *testing.T) { + t.Run("ok", func(t *testing.T) { + // 200 + ctx := newTestClient(t) + did := did.MustParseDID("did:nuts:123") + ctx.vdrInstance.EXPECT().IsOwner(nil, did).Return(true, nil) + + res, err := ctx.client.GetOAuthClientMetadata(nil, GetOAuthClientMetadataRequestObject{Id: did.ID}) + + require.NoError(t, err) + assert.IsType(t, GetOAuthClientMetadata200JSONResponse{}, res) + }) + t.Run("error - not a did", func(t *testing.T) { + //400 + ctx := newTestClient(t) + + res, err := ctx.client.GetOAuthClientMetadata(nil, GetOAuthClientMetadataRequestObject{}) + + assert.Equal(t, 400, statusCodeFrom(err)) + assert.EqualError(t, err, "client metadata: invalid DID") + assert.Nil(t, res) + }) + t.Run("error - contains full did:nuts", func(t *testing.T) { + //400 + ctx := newTestClient(t) + + res, err := ctx.client.GetOAuthClientMetadata(nil, GetOAuthClientMetadataRequestObject{Id: "did:nuts:123"}) + + assert.Equal(t, 400, statusCodeFrom(err)) + assert.EqualError(t, err, "client metadata: id contains full did") + assert.Nil(t, res) + }) + t.Run("error - did not managed by this node", func(t *testing.T) { + //404 + ctx := newTestClient(t) + did := did.MustParseDID("did:nuts:123") + ctx.vdrInstance.EXPECT().IsOwner(nil, did) + + res, err := ctx.client.GetOAuthClientMetadata(nil, GetOAuthClientMetadataRequestObject{Id: did.ID}) + + assert.Equal(t, 404, statusCodeFrom(err)) + assert.EqualError(t, err, "client metadata: did not owned") + assert.Nil(t, res) + }) + t.Run("error - did does not exist", func(t *testing.T) { + //404 + ctx := newTestClient(t) + did := did.MustParseDID("did:nuts:123") + ctx.vdrInstance.EXPECT().IsOwner(nil, did).Return(false, vdr.ErrNotFound) + + res, err := ctx.client.GetOAuthClientMetadata(nil, GetOAuthClientMetadataRequestObject{Id: did.ID}) + + assert.Equal(t, 404, statusCodeFrom(err)) + assert.EqualError(t, err, "client metadata: unable to find the DID document") + assert.Nil(t, res) + }) + t.Run("error - internal error 500", func(t *testing.T) { + //500 + ctx := newTestClient(t) + did := did.MustParseDID("did:nuts:123") + ctx.vdrInstance.EXPECT().IsOwner(nil, did).Return(false, errors.New("unknown error")) + + res, err := ctx.client.GetOAuthClientMetadata(nil, GetOAuthClientMetadataRequestObject{Id: did.ID}) + + assert.Equal(t, 500, statusCodeFrom(err)) + assert.EqualError(t, err, "client metadata: unknown error") + assert.Nil(t, res) + }) +} + // statusCodeFrom returns the statuscode if err is core.HTTPStatusCodeError, or 0 if it isn't func statusCodeFrom(err error) int { var SE core.HTTPStatusCodeError diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index ae99c16334..8fab3b5092 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -142,6 +142,9 @@ type ServerInterface interface { // Used by to request access- or refresh tokens. // (POST /iam/{did}/token) HandleTokenRequest(ctx echo.Context, did string) error + // Get the OAuth2 Client metadata + // (GET /iam/{id}/oauth-client) + GetOAuthClientMetadata(ctx echo.Context, id string) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -222,6 +225,22 @@ func (w *ServerInterfaceWrapper) HandleTokenRequest(ctx echo.Context) error { return err } +// GetOAuthClientMetadata converts echo context to params. +func (w *ServerInterfaceWrapper) GetOAuthClientMetadata(ctx echo.Context) error { + var err error + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetOAuthClientMetadata(ctx, id) + return err +} + // This is a simple interface which specifies echo.Route addition functions which // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration @@ -254,6 +273,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/iam/:did/authorize", wrapper.HandleAuthorizeRequest) router.GET(baseURL+"/iam/:did/did.json", wrapper.GetWebDID) router.POST(baseURL+"/iam/:did/token", wrapper.HandleTokenRequest) + router.GET(baseURL+"/iam/:id/oauth-client", wrapper.GetOAuthClientMetadata) } @@ -398,6 +418,44 @@ func (response HandleTokenRequest404JSONResponse) VisitHandleTokenRequestRespons return json.NewEncoder(w).Encode(response) } +type GetOAuthClientMetadataRequestObject struct { + Id string `json:"id"` +} + +type GetOAuthClientMetadataResponseObject interface { + VisitGetOAuthClientMetadataResponse(w http.ResponseWriter) error +} + +type GetOAuthClientMetadata200JSONResponse OAuthClientMetadata + +func (response GetOAuthClientMetadata200JSONResponse) VisitGetOAuthClientMetadataResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetOAuthClientMetadatadefaultApplicationProblemPlusJSONResponse struct { + Body struct { + // Detail A human-readable explanation specific to this occurrence of the problem. + Detail string `json:"detail"` + + // Status HTTP statuscode + Status float32 `json:"status"` + + // Title A short, human-readable summary of the problem type. + Title string `json:"title"` + } + StatusCode int +} + +func (response GetOAuthClientMetadatadefaultApplicationProblemPlusJSONResponse) VisitGetOAuthClientMetadataResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // Get the OAuth2 Authorization Server metadata @@ -412,6 +470,9 @@ type StrictServerInterface interface { // Used by to request access- or refresh tokens. // (POST /iam/{did}/token) HandleTokenRequest(ctx context.Context, request HandleTokenRequestRequestObject) (HandleTokenRequestResponseObject, error) + // Get the OAuth2 Client metadata + // (GET /iam/{id}/oauth-client) + GetOAuthClientMetadata(ctx context.Context, request GetOAuthClientMetadataRequestObject) (GetOAuthClientMetadataResponseObject, error) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -536,3 +597,28 @@ func (sh *strictHandler) HandleTokenRequest(ctx echo.Context, did string) error } return nil } + +// GetOAuthClientMetadata operation middleware +func (sh *strictHandler) GetOAuthClientMetadata(ctx echo.Context, id string) error { + var request GetOAuthClientMetadataRequestObject + + request.Id = id + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetOAuthClientMetadata(ctx.Request().Context(), request.(GetOAuthClientMetadataRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetOAuthClientMetadata") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetOAuthClientMetadataResponseObject); ok { + return validResponse.VisitGetOAuthClientMetadataResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} diff --git a/auth/api/iam/implementation_documentation/metadata_types.go b/auth/api/iam/implementation_documentation/metadata_types.go index f03cfdb70c..7f4d5a49d7 100644 --- a/auth/api/iam/implementation_documentation/metadata_types.go +++ b/auth/api/iam/implementation_documentation/metadata_types.go @@ -280,3 +280,273 @@ type OpenID4VCIFields struct { PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported"` //OPTIONAL. A JSON Boolean indicating whether the issuer accepts a Token Request with a Pre-Authorized Code but without a client id. The default is false. } + +/********** OAuthClientMetadata **********/ +// TODO: our custom /.well-known/openid-credential-wallet endpoint returns the Oauth Client Metadata +// Clients should either be `pre-registered` meaning that they have this value for `client_id_scheme` in +// When `client_id_scheme` is empty or `pre-registered` + +// OAuthClientMetadata (dynamic client registration) +// Full registry at https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#client-metadata +type RFC7591Fields struct { + RedirectURIs []string `json:"redirect_uris"` + //Array of redirection URI strings for use in redirect-based flows + //such as the authorization code and implicit flows. As required by + //Section 2 of OAuth 2.0 [RFC6749], clients using flows with + //redirection MUST register their redirection URI values. + //Authorization servers that support dynamic registration for + //redirect-based flows MUST implement support for this metadata + //value. + + // RFC8705 describes mTLS https://www.rfc-editor.org/rfc/rfc8705.html + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` + //REQUIRED String indicator of the requested authentication method for the + //token endpoint. Values defined by this specification are: + // + // * "none": The client is a public client as defined in OAuth 2.0, + // Section 2.1, and does not have a client secret. + // + // * "client_secret_post": The client uses the HTTP POST parameters + // as defined in OAuth 2.0, Section 2.3.1. + // + // * "client_secret_basic": The client uses HTTP Basic as defined in + // OAuth 2.0, Section 2.3.1. + // + //Additional values can be defined via the IANA "OAuth Token + //Endpoint Authentication Methods" registry established in + //Section 4.2. Absolute URIs can also be used as values for this + //parameter without being registered. If unspecified or omitted, + //the default is "client_secret_basic", denoting the HTTP Basic + //authentication scheme as specified in Section 2.3.1 of OAuth 2.0. + + GrantTypes []string `json:"grant_types"` + //Array of OAuth 2.0 grant type strings that the client can use at + //the token endpoint. These grant types are defined as follows: + // + //* "authorization_code": The authorization code grant type defined + //in OAuth 2.0, Section 4.1. + // + //* "implicit": The implicit grant type defined in OAuth 2.0, + //Section 4.2. + // + //* "password": The resource owner password credentials grant type + //defined in OAuth 2.0, Section 4.3. + // + //* "client_credentials": The client credentials grant type defined + //in OAuth 2.0, Section 4.4. + // + //* "refresh_token": The refresh token grant type defined in OAuth + //2.0, Section 6. + // + //* "urn:ietf:params:oauth:grant-type:jwt-bearer": The JWT Bearer + //Token Grant Type defined in OAuth JWT Bearer Token Profiles [RFC7523]. + // + //* "urn:ietf:params:oauth:grant-type:saml2-bearer": The SAML 2.0 + //Bearer Assertion Grant defined in OAuth SAML 2 Bearer Token + //Profiles [RFC7522]. + // + //If the token endpoint is used in the grant type, the value of this + //parameter MUST be the same as the value of the "grant_type" + //parameter passed to the token endpoint defined in the grant type + //definition. Authorization servers MAY allow for other values as + //defined in the grant type extension process described in OAuth + //2.0, Section 4.5. If omitted, the default behavior is that the + //client will use only the "authorization_code" Grant Type. + + ResponseTypes []string `json:"response_types"` + //Array of the OAuth 2.0 response type strings that the client can + //use at the authorization endpoint. These response types are + //defined as follows: + // + //* "code": The authorization code response type defined in OAuth + //2.0, Section 4.1. + // + //* "token": The implicit response type defined in OAuth 2.0, + //Section 4.2. + // + //If the authorization endpoint is used by the grant type, the value + //of this parameter MUST be the same as the value of the + //"response_type" parameter passed to the authorization endpoint + //defined in the grant type definition. Authorization servers MAY + //allow for other values as defined in the grant type extension + //process is described in OAuth 2.0, Section 4.5. If omitted, the + //default is that the client will use only the "code" response type. + + ClientName string `json:"client_name"` + //Human-readable string name of the client to be presented to the + //end-user during authorization. If omitted, the authorization + //server MAY display the raw "client_id" value to the end-user + //instead. It is RECOMMENDED that clients always send this field. + //The value of this field MAY be internationalized, as described in + //Section 2.2. + + ClientURI string `json:"client_uri"` + //URL string of a web page providing information about the client. + //If present, the server SHOULD display this URL to the end-user in + //a clickable fashion. It is RECOMMENDED that clients always send + //this field. The value of this field MUST point to a valid web + //page. The value of this field MAY be internationalized, as + //described in Section 2.2. + + LogoURI string `json:"logo_uri"` + //URL string that references a logo for the client. If present, the + //server SHOULD display this image to the end-user during approval. + //The value of this field MUST point to a valid image file. The + //value of this field MAY be internationalized, as described in + //Section 2.2. + + Scope string `json:"scope"` + //String containing a space-separated list of scope values (as + //described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client + //can use when requesting access tokens. The semantics of values in + //this list are service specific. If omitted, an authorization + //server MAY register a client with a default set of scopes. + + Contacts []string `json:"contacts"` + //OPTIONAL: Array of strings representing ways to contact people responsible + //for this client, typically email addresses. The authorization + //server MAY make these contact addresses available to end-users for + //support requests for the client. See Section 6 for information on + //Privacy Considerations. + + TosURI string `json:"tos_uri"` + //URL string that points to a human-readable terms of service + //document for the client that describes a contractual relationship + //between the end-user and the client that the end-user accepts when + //authorizing the client. The authorization server SHOULD display + //this URL to the end-user if it is provided. The value of this + //field MUST point to a valid web page. The value of this field MAY + //be internationalized, as described in Section 2.2. + + PolicyURI string `json:"policy_uri"` + //URL string that points to a human-readable privacy policy document + //that describes how the deployment organization collects, uses, + //retains, and discloses personal data. The authorization server + //SHOULD display this URL to the end-user if it is provided. The + //value of this field MUST point to a valid web page. The value of + //this field MAY be internationalized, as described in Section 2.2. + + JwksURI string `json:"jwks_uri"` + //URL string referencing the client's JSON Web Key (JWK) Set + //[RFC7517] document, which contains the client's public keys. The + //value of this field MUST point to a valid JWK Set document. These + //keys can be used by higher-level protocols that use signing or + //encryption. For instance, these keys might be used by some + //applications for validating signed requests made to the token + //endpoint when using JWTs for client authentication [RFC7523]. Use + //of this parameter is preferred over the "jwks" parameter, as it + //allows for easier key rotation. The "jwks_uri" and "jwks" + //parameters MUST NOT both be present in the same request or + //response. + + Jwks any `json:"jwks"` + //Client's JSON Web Key Set [RFC7517] document value, which contains + //the client's public keys. The value of this field MUST be a JSON + //object containing a valid JWK Set. These keys can be used by + //higher-level protocols that use signing or encryption. This + //parameter is intended to be used by clients that cannot use the + //"jwks_uri" parameter, such as native clients that cannot host + //public URLs. The "jwks_uri" and "jwks" parameters MUST NOT both + //be present in the same request or response. + + SoftwareID string `json:"software_id"` + //A unique identifier string (e.g., a Universally Unique Identifier + //(UUID)) assigned by the client developer or software publisher + //used by registration endpoints to identify the client software to + //be dynamically registered. Unlike "client_id", which is issued by + //the authorization server and SHOULD vary between instances, the + //"software_id" SHOULD remain the same for all instances of the + //client software. The "software_id" SHOULD remain the same across + //multiple updates or versions of the same piece of software. The + //value of this field is not intended to be human readable and is + //usually opaque to the client and authorization server. + + SoftwareVersion string `json:"software_version"` + //A version identifier string for the client software identified by + //"software_id". The value of the "software_version" SHOULD change + //on any update to the client software identified by the same + //"software_id". The value of this field is intended to be compared + //using string equality matching and no other comparison semantics + //are defined by this specification. The value of this field is + //outside the scope of this specification, but it is not intended to + //be human readable and is usually opaque to the client and + //authorization server. The definition of what constitutes an + //update to client software that would trigger a change to this + //value is specific to the software itself and is outside the scope + //of this specification. +} + +// https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-client-metadata +type OpenID4VCIClientMetadataFields struct { + // On a server/organization wallet the credentials are accepted automatically after receiving an offer since there is no user for manual approval. + // To prevent spamming the endpoint needs some form of protection. Currently the endpoint is on n2n which requires mTLS. + // (We need to look into OAuth PKI client authentication. It is not described for the credential offer, + // but the NutsNode could refuse any offer if the /token endpoint where the offer is exchanged for credential does not support this. https://www.rfc-editor.org/rfc/rfc8705.html) + CredentialOfferEndpoint string `json:"credential_offer_endpoint"` + //OPTIONAL. Credential Offer Endpoint of a Wallet. +} + +// TODO: per convention the Client Metadata values are `key_name(s)` (plural if the value's type can be plural) followed by all supported values +// +// ------------- while the Server Metadata uses value `key_names_supported` (always plural) for the list of supported values. +// +// https://openid.bitbucket.io/connect/openid-4-verifiable-presentations-1_0.html#name-verifier-metadata-client-me +type OpenID4VPClientMetadataFields struct { + VPFormats any `json:"vp_formats"` + //REQUIRED. An object defining the formats and proof types of Verifiable Presentations and Verifiable Credentials + //that a Verifier supports. For specific values that can be used, see Appendix A. Deployments can extend the formats supported, + //provided Issuers, Holders and Verifiers all understand the new format. + + // := did + ClientIdScheme string `json:"client_id_scheme"` + //OPTIONAL. JSON String identifying the Client Identifier scheme. The value range defined by this specification is + //pre-registered, redirect_uri, entity_id, did. If omitted, the default value is pre-registered. +} + +// TODO: EBSI uses server metadata convention for client metadata (see TODO on OpenID4VPClientMetadataFields) +// https://api-conformance.ebsi.eu/docs/ct/providers-and-wallets-metadata#holder-wallet-metadata +// Holder Wallets are non-reachable, and they can use the client_metadata field in the initial Authorisation Request to deliver the configuration, +// the default configuration will be used if not explicitly provided. All provided fields in client_metadata will fully replace the default properties. +type EBSIHolderWalletMeta struct { + AuthorizationEndpoint string `json:"authorization_endpoint"` + //OPTIONAL URL of the authorization server's authorization endpoint + ScopesSupported []string `json:"scopes_supported"` + //OPTIONAL A JSON array containing a list of the OAuth 2.0 "scope" values that this authorization server supports + ResponseTypesSupported []string `json:"response_types_supported"` + //OPTIONAL A JSON array containing a list of the OAuth 2.0 "response_type" values that this authorization server supports + SubjectTypesSupported []string `json:"subject_types_supported"` + //OPTIONAL A JSON array containing a list of the Subject Identifier types that this OP supports + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` + //OPTIONAL A JSON array containing a list of the JWS "alg" values supported by the OP for the ID Token + RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"` + //OPTIONAL A JSON array containing a list of the JWS "alg" values supported by the OP for Request Objects + VPFormatsSupported any `json:"vp_formats_supported"` + //REQUIRED A JSON Object containing a list of key value pairs, where the key is a string identifying a Credential format supported by the Wallet + //vp_formats_supported + //.jwt_vp + ////OPTIONAL A JSON Object, defining support for Verifiable Presentations in JWT format + ////vp_formats_supported + ////.jwt_vp + ////.alg_values_supported + //////OPTIONAL A JSON Array of case sensitive strings that identify the cryptographic suites that are supported + //vp_formats_supported + //.jwt_vc + ////OPTIONAL A JSON Object, defining support for Verifiable Credentials in JWT format + ////vp_formats_supported + ////.jwt_vc + ////.alg_values_supported + //////OPTIONAL A JSON Array of case sensitive strings that identify the cryptographic suites that are supported + SubjectSyntaxTypesSupported []string `json:"subject_syntax_types_supported"` + //OPTIONAL A JSON Array of supported DID methods and their possible sub types. + IDTokenTypesSupported []string `json:"id_token_types_supported"` + //OPTIONAL A JSON array of strings containing the list of ID Token types supported by the OP + + // TODO: EBSI aso defines Service Wallet Metadata https://api-conformance.ebsi.eu/docs/ct/providers-and-wallets-metadata#service-wallet-metadata + // Service Wallet is a decoupled construct, having a composite relationship with an Issuer or a Verifier, with purpose of requesting, signing and managing Verifiable Credentials. + // These share the same client_id as the main functionality (verification or issuance), but expose own configuration through client_metadata in Authorisation Request. + // The configuration contains all fields from Holder Wallet Metadata, and the following extension. + JwksURI string `json:"jwks_uri"` + //REQUIRED URL of the authorization server's JWK Set document. Must start with client_id. +} + +// TODO - EBSI states: Authorisation Server will expose /.well-known/openid-configuration while. This is only mentioned in SIOPv2, others use /.well-known/authorization-server-metadata diff --git a/auth/api/iam/metadata.go b/auth/api/iam/metadata.go index 359ce06dda..21977ea927 100644 --- a/auth/api/iam/metadata.go +++ b/auth/api/iam/metadata.go @@ -21,6 +21,7 @@ package iam import ( "github.com/nuts-foundation/nuts-node/core" "net/url" + "strings" ) const ( @@ -62,3 +63,23 @@ func authorizationServerMetadata(identity url.URL) OAuthAuthorizationServerMetad ClientIdSchemesSupported: clientIdSchemesSupported, } } + +// clientMetadata should only be used for dids managed by the node. It assumes the provided identity URL is correct. +func clientMetadata(identity url.URL) OAuthClientMetadata { + softwareID, softwareVersion, _ := strings.Cut(core.UserAgent(), "/") + return OAuthClientMetadata{ + //RedirectURIs: nil, + TokenEndpointAuthMethod: "none", // defaults is "client_secret_basic" if not provided + GrantTypes: grantTypesSupported, + ResponseTypes: responseTypesSupported, + //Scope: "", + //Contacts: nil, + //JwksURI: "", + //Jwks: nil, + SoftwareID: softwareID, // nuts-node-refimpl + SoftwareVersion: softwareVersion, // version tag or "unknown" + //CredentialOfferEndpoint: "", + VPFormats: vpFormatsSupported, + ClientIdScheme: "did", + } +} diff --git a/auth/api/iam/metadata_test.go b/auth/api/iam/metadata_test.go index a657c52365..2a9965c5d2 100644 --- a/auth/api/iam/metadata_test.go +++ b/auth/api/iam/metadata_test.go @@ -19,6 +19,7 @@ package iam import ( + "github.com/nuts-foundation/nuts-node/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net/url" @@ -64,15 +65,16 @@ func TestIssuerIdToWellKnown(t *testing.T) { }) } +var vpFormats = map[string]map[string][]string{ + "jwt_vc_json": {"alg_values_supported": []string{"PS256", "PS384", "PS512", "ES256", "ES384", "ES512"}}, + "jwt_vp_json": {"alg_values_supported": []string{"PS256", "PS384", "PS512", "ES256", "ES384", "ES512"}}, + "ldp_vc": {"proof_type_values_supported": []string{"JsonWebSignature2020"}}, + "ldp_vp": {"proof_type_values_supported": []string{"JsonWebSignature2020"}}, +} + func Test_authorizationServerMetadata(t *testing.T) { identity := "https://example.com/iam/did:nuts:123" identityURL, _ := url.Parse(identity) - vpFormats := map[string]map[string][]string{ - "jwt_vc_json": {"alg_values_supported": []string{"PS256", "PS384", "PS512", "ES256", "ES384", "ES512"}}, - "jwt_vp_json": {"alg_values_supported": []string{"PS256", "PS384", "PS512", "ES256", "ES384", "ES512"}}, - "ldp_vc": {"proof_type_values_supported": []string{"JsonWebSignature2020"}}, - "ldp_vp": {"proof_type_values_supported": []string{"JsonWebSignature2020"}}, - } expected := OAuthAuthorizationServerMetadata{ Issuer: identity, AuthorizationEndpoint: identity + "/authorize", @@ -81,9 +83,29 @@ func Test_authorizationServerMetadata(t *testing.T) { TokenEndpoint: identity + "/token", GrantTypesSupported: []string{"authorization_code", "vp_token", "urn:ietf:params:oauth:grant-type:pre-authorized_code"}, PreAuthorizedGrantAnonymousAccessSupported: true, - VPFormats: vpFormats, - VPFormatsSupported: vpFormats, + VPFormats: vpFormatsSupported, + VPFormatsSupported: vpFormatsSupported, ClientIdSchemesSupported: []string{"did"}, } assert.Equal(t, expected, authorizationServerMetadata(*identityURL)) } + +func Test_clientMetadata(t *testing.T) { + core.GitVersion = "testVersion" + expected := OAuthClientMetadata{ + RedirectURIs: nil, + TokenEndpointAuthMethod: "none", + GrantTypes: []string{"authorization_code", "vp_token", "urn:ietf:params:oauth:grant-type:pre-authorized_code"}, + ResponseTypes: []string{"code", "vp_token", "vp_token id_token"}, + Scope: "", + Contacts: nil, + JwksURI: "", + Jwks: nil, + SoftwareID: "nuts-node-refimpl", + SoftwareVersion: "testVersion", + CredentialOfferEndpoint: "", + VPFormats: vpFormatsSupported, + ClientIdScheme: "did", + } + assert.Equal(t, expected, clientMetadata(url.URL{})) +} diff --git a/auth/api/iam/types.go b/auth/api/iam/types.go index 54cbc7d2d3..4a85bce4d5 100644 --- a/auth/api/iam/types.go +++ b/auth/api/iam/types.go @@ -23,6 +23,12 @@ import ( "github.com/nuts-foundation/nuts-node/vdr/types" ) +// DIDDocument is an alias +type DIDDocument = did.Document + +// DIDDocumentMetadata is an alias +type DIDDocumentMetadata = types.DocumentMetadata + const ( // responseTypeParam is the name of the response_type parameter. // Specified by https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1 @@ -192,7 +198,7 @@ type OAuthAuthorizationServerMetadata struct { // PreAuthorizedGrantAnonymousAccessSupported indicates whether anonymous access (requests without client_id) for pre-authorized code grant flows. // See https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-oauth-20-authorization-serv - PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported"` + PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported,omitempty"` // PresentationDefinitionUriSupported specifies whether the Wallet supports the transfer of presentation_definition by reference, with true indicating support. // If omitted, the default value is true. (hence pointer, or add custom unmarshalling) @@ -209,8 +215,73 @@ type OAuthAuthorizationServerMetadata struct { ClientIdSchemesSupported []string `json:"client_id_schemes_supported,omitempty"` } -// DIDDocument is an alias -type DIDDocument = did.Document - -// DIDDocumentMetadata is an alias -type DIDDocumentMetadata = types.DocumentMetadata \ No newline at end of file +// OAuthClientMetadata defines the OAuth Client metadata. +// Specified by https://www.rfc-editor.org/rfc/rfc7591.html and elsewhere. +type OAuthClientMetadata struct { + // RedirectURIs lists all URIs that the client may use in any redirect-based flow. + // From https://www.rfc-editor.org/rfc/rfc7591.html + RedirectURIs []string `json:"redirect_uris,omitempty"` + + // TODO: What do we use? Must provide a value if its not "client_secret_basic" + // TokenEndpointAuthMethod indicator of the requested authentication method for the token endpoint. + // If unspecified or omitted, the default is "client_secret_basic", denoting the HTTP Basic authentication scheme as specified in Section 2.3.1 of OAuth 2.0. + // Examples are: none, client_secret_post, client_secret_basic, tls_client_auth. + // From https://www.rfc-editor.org/rfc/rfc7591.html + // TODO: Can "tls_client_auth" replace /n2n/ for pre-authorized_code flow? https://www.rfc-editor.org/rfc/rfc8705.html + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` + + // GrantTypes lists all supported grant_types. Defaults to "authorization_code" if omitted. + // From https://www.rfc-editor.org/rfc/rfc7591.html + GrantTypes []string `json:"grant_types,omitempty"` + + // ResponseTypes lists all supported response_types. Defaults to "code". Must contain the values corresponding to listed GrantTypes. + // From https://www.rfc-editor.org/rfc/rfc7591.html + ResponseTypes []string `json:"response_types,omitempty"` + + // Scope contains a space-separated list of scopes the client can request. + // From https://www.rfc-editor.org/rfc/rfc7591.html + // TODO: I don't see the use for this. The idea is that an AS does not assign scopes to a client that it does not support (or wants to request at any time), but seems like unnecessary complexity for minimal safety. + Scope string `json:"scope,omitempty"` + + // Contacts contains an array of strings representing ways to contact people responsible for this client, typically email addresses. + // From https://www.rfc-editor.org/rfc/rfc7591.html + // TODO: remove? Can plug DID docs contact info. + Contacts []string `json:"contacts,omitempty"` + + // JwksURI URL string referencing the client's JSON Web Key (JWK) Set [RFC7517] document, which contains the client's public keys. + // From https://www.rfc-editor.org/rfc/rfc7591.html + // TODO: remove? Can list the DID's keys. Could be useful if authorization without DIDs/VCs is needed. + // TODO: In EBSI it is a required field for the Service Wallet Metadata https://api-conformance.ebsi.eu/docs/ct/providers-and-wallets-metadata#service-wallet-metadata + JwksURI string `json:"jwks_uri,omitempty"` + // Jwks includes the JWK Set of a client. Mutually exclusive with JwksURI. + // From https://www.rfc-editor.org/rfc/rfc7591.html + Jwks any `json:"jwks,omitempty"` + + // SoftwareID is a unique identifier string (e.g., a Universally Unique Identifier (UUID)) assigned by the client developer. + // From https://www.rfc-editor.org/rfc/rfc7591.html + SoftwareID string `json:"software_id,omitempty"` + // SoftwareVersion is a version identifier string for the client software identified by "software_id". + // From https://www.rfc-editor.org/rfc/rfc7591.html + // TODO: Including a software_id + software_version could provide us with some upgrade paths in the future. + SoftwareVersion string `json:"software_version,omitempty"` + + // TODO: ignored values: client_name, client_uri, logo_uri, tos_uri, policy_uri. + // TODO: Things like client_name and logo may enhance the user experience when asking to accept authorization requests, but this should probably be added on the server size for that? + + /*********** OpenID4VCI ***********/ + + // CredentialOfferEndpoint contains a URL where the pre-authorized_code flow offers a credential. + // https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-client-metadata + // TODO: openid4vci duplicate. Also defined on /.well-known/openid-credential-wallet to be /n2n/identity/{did}/openid4vci/credential_offer + CredentialOfferEndpoint string `json:"credential_offer_endpoint,omitempty"` + + /*********** OpenID4VP ***********/ + // VPFormats lists the vp_formats supported by the client. See additional comments on vpFormatsSupported. + // https://openid.bitbucket.io/connect/openid-4-verifiable-presentations-1_0.html#name-verifier-metadata-client-me + VPFormats any `json:"vp_formats,omitempty"` + + // ClientIdScheme is a string identifying the Client Identifier scheme. The value range defined by this specification is + // pre-registered, redirect_uri, entity_id, did. If omitted, the default value is pre-registered. + // https://openid.bitbucket.io/connect/openid-4-verifiable-presentations-1_0.html#name-verifier-metadata-client-me + ClientIdScheme string `json:"client_id_scheme,omitempty"` +} diff --git a/codegen/configs/auth_iam.yaml b/codegen/configs/auth_iam.yaml index 02335ce0ea..c2d29e3163 100644 --- a/codegen/configs/auth_iam.yaml +++ b/codegen/configs/auth_iam.yaml @@ -7,4 +7,5 @@ output-options: skip-prune: false exclude-schemas: - DIDDocument - - OAuthAuthorizationServerMetadata \ No newline at end of file + - OAuthAuthorizationServerMetadata + - OAuthClientMetadata diff --git a/docs/_static/auth/iam.yaml b/docs/_static/auth/iam.yaml index f2b8bcf1c6..c23fc7f02e 100644 --- a/docs/_static/auth/iam.yaml +++ b/docs/_static/auth/iam.yaml @@ -162,6 +162,38 @@ paths: "$ref": "#/components/schemas/OAuthAuthorizationServerMetadata" default: $ref: '../common/error_response.yaml' + /iam/{id}/oauth-client: + get: + tags: + - well-known + summary: Get the OAuth2 Client metadata + description: > + Returns relevant OAuth Client metadata as defined in + https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#client-metadata + and other OpenID4VC specification set. + + error returns: + * 400 - invalid input + * 404 - did not found; possibly be non-existing, deactivated, or not managed by this node + * 500 - internal server error + operationId: getOAuthClientMetadata + parameters: + - name: id + in: path + required: true + schema: + description: must be the key fingerprint identifying the did:web address + type: string + example: NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs + responses: + "200": + description: OK + content: + application/json: + schema: + "$ref": "#/components/schemas/OAuthClientMetadata" + default: + $ref: '../common/error_response.yaml' # /internal/auth/v2/{did}/request-presentation: # post: # operationId: requestPresentation @@ -294,7 +326,12 @@ components: OAuthAuthorizationServerMetadata: description: | OAuth2 Authorization Server Metadata - Contain properties from several specifications and will grow over time + Contain properties from several specifications and may grow over time + type: object + OAuthClientMetadata: + description: | + OAuth2 Client Metadata + Contain properties from several specifications and may grow over time type: object ErrorResponse: type: object