Skip to content

France Identitรฉ Partenaires โ€” OID4VCI Draft 13

โ† Back to the Partenaires app overview

OID4VCI Draft 13 profile

Compatibility profile

This section documents the OID4VCI Draft 13 profile currently exercised with the France Identitรฉ Partenaires test application. Draft 13 is an older implementer's draft, not the current OID4VCI 1.0 final specification. Keep the Draft 13 and final 1.0 protocol surfaces separate.

The profile is intended for a partner that operates its own credential issuer and wants to test issuance into France Identitรฉ Partenaires. The issuer remains responsible for the HTTPS endpoints, OAuth token service, holder-binding proof validation, credential signing, and the trust material required by its test environment. France Identitรฉ acts as the wallet that receives and stores the credential.

The current Partenaires Draft 13 test path has one supported issuance target: the IDAK bicycle attestation. The offer, issuer metadata, Credential Request, and returned credential must all describe the fr.idak.mbicycle.1 credential as an ISO/IEC 18013-5 mdoc. Other credential configurations and SD-JWT VC are outside the supported test scope documented here.

Profile at a glance

Property Partenaires Draft 13 profile
Issuance flow Pre-authorized code flow
Issuer identifier A dedicated profile URL ending in /protocol/oid4vci/draft-13
Credential Configuration ID fr.idak.mbicycle.1
Credential format mso_mdoc only
Doctype fr.idak.mbicycle.1
mdoc binding cose_key
Tested proof JWT proof with ES256 and a P-256 holder key
Token request application/x-www-form-urlencoded
Access-token scheme Ordinary Bearer; return token_type as lowercase bearer for Partenaires compatibility
Proof nonce c_nonce returned by the current token response
Credential request Singular proof object
Credential response Singular credential member containing the mdoc
Separate nonce endpoint Not part of this profile
DPoP / client attestation Not part of this profile

The profile intentionally uses a small metadata projection. Optional metadata must not contain malformed values: in particular, an advertised endpoint must be an absolute HTTPS URI, and a logo or other display URI must be a valid URI if it is published.

Known-good reference

The live playground mdoc issuer exposes the profile here:

Inspect the live Draft 13 issuer metadata

The current reference configuration is:

Field Value
Credential Configuration ID fr.idak.mbicycle.1
Format mso_mdoc
Doctype fr.idak.mbicycle.1
Binding method cose_key
Issuer signing algorithm ES256
Proof type jwt
Proof signing algorithm ES256

The issuer identifier and endpoint URLs belong to the partner's service. The credential values in this table are the Partenaires test contract: do not replace fr.idak.mbicycle.1, mso_mdoc, or the doctype with a PID, PhotoID, mDL, SD-JWT VC, or another custom configuration.

Keep Draft 13 separate from OID4VCI 1.0

If an issuer also supports OID4VCI 1.0, expose two distinct protocol surfaces. Do not select the protocol by changing a query parameter while keeping the same issuer identifier, and do not allow a wallet to cache one profile's metadata for the other profile.

Draft 13 Do not substitute the final 1.0 form
Issuer identifier includes /protocol/oid4vci/draft-13 The canonical final issuer URL
Singular proof proofs
Proof nonce from the token response A separate final-profile nonce exchange
Authorization: Bearer <access_token> Authorization: DPoP <access_token>
Singular credential response Do not require a final-profile credentials array

The OID4VCI 1.0 external issuance profile describes a different integration. Do not copy its DPoP, client-attestation, JWE, or nonce-endpoint steps into a Draft 13 transaction.

1. Choose the issuer identifier and endpoints

Choose the partner's canonical registered issuer URL. The Draft 13 profile issuer is that URL with /protocol/oid4vci/draft-13 appended. For example, if the partner base issuer is https://issuer.example.test/oid4vci, the profile issuer and metadata URL are:

https://issuer.example.test/oid4vci/protocol/oid4vci/draft-13
https://issuer.example.test/oid4vci/protocol/oid4vci/draft-13/.well-known/openid-credential-issuer

The profile issuer is case-sensitive, uses https, and has no query or fragment. Remove a trailing slash before appending the profile suffix.

The credential_issuer value in the metadata, the Credential Offer, and the proof JWT aud claim must be the same exact profile issuer URL. Do not use the token endpoint, credential endpoint, metadata URL, or the unsuffixed issuer URL as the proof audience.

After discovery, use the endpoint values returned in metadata. Do not derive them from a hostname or replace them with relative paths.

2. Publish issuer metadata

Serve the metadata endpoint with:

GET /.well-known/openid-credential-issuer HTTP/1.1
Accept: application/json

The successful response must be JSON with status 200 and Content-Type: application/json. The endpoint and all advertised URLs must be reachable over TLS.

Minimal mdoc metadata

The top-level token_endpoint, grant_types_supported, and singular token_endpoint_auth_method fields below reflect the metadata shape consumed by the Partenaires compatibility path. They are deliberately kept alongside the normal OID4VCI metadata. If the issuer publishes a separate OAuth Authorization Server metadata document, it must remain standards-compliant and use the appropriate RFC 8414 field names, including plural token_endpoint_auth_methods_supported.

{
  "credential_issuer": "https://issuer.example.test/oid4vci/protocol/oid4vci/draft-13",
  "credential_endpoint": "https://issuer.example.test/oid4vci/protocol/oid4vci/draft-13/credential",
  "token_endpoint": "https://issuer.example.test/oid4vci/protocol/oid4vci/draft-13/token",
  "notification_endpoint": "https://issuer.example.test/oid4vci/protocol/oid4vci/draft-13/notification",
  "grant_types_supported": [
    "urn:ietf:params:oauth:grant-type:pre-authorized_code"
  ],
  "token_endpoint_auth_method": [
    "none"
  ],
  "credential_configurations_supported": {
    "fr.idak.mbicycle.1": {
      "format": "mso_mdoc",
      "doctype": "fr.idak.mbicycle.1",
      "cryptographic_binding_methods_supported": [
        "cose_key"
      ],
      "credential_signing_alg_values_supported": [
        "ES256"
      ],
      "proof_types_supported": {
        "jwt": {
          "proof_signing_alg_values_supported": [
            "ES256"
          ]
        }
      }
    }
  }
}

For this Partenaires test path, the configuration key and doctype are both fr.idak.mbicycle.1. They remain separate protocol fields: publish and validate each field explicitly.

Metadata rules

  • credential_issuer is the profile issuer identifier, not the base issuer identifier.
  • credential_endpoint, token_endpoint, and notification_endpoint must be complete absolute HTTPS URIs. Do not publish /credential, an empty string, null, or a URI with a typo.
  • notification_endpoint is optional. Omit it if the issuer does not implement notifications. If it is present, implement the endpoint before testing.
  • grant_types_supported must include urn:ietf:params:oauth:grant-type:pre-authorized_code.
  • token_endpoint_auth_method: ["none"] means that this test flow does not require client authentication at the token endpoint.
  • credential_configurations_supported is a map keyed by the Credential Configuration ID. For this test path it must contain fr.idak.mbicycle.1 with format: "mso_mdoc" and doctype: "fr.idak.mbicycle.1".
  • For the mdoc configuration, publish the supported binding method, issuer signing algorithms, and proof types and algorithms shown above. Additional credential configurations are outside the current Partenaires issuance test scope.
  • Publish only algorithms that the issuer and the Partenaires test path actually support. The known-good profile advertises JWT/ES256 only.
  • This compatibility profile does not advertise a nonce_endpoint. Do not add a Final 1.0 nonce endpoint and expect the Partenaires Draft 13 flow to call it.
  • The live profile does not use authorization_servers indirection. Keep the Draft 13 metadata self-contained unless a separately tested deployment requires an authorization server.
  • Use Cache-Control: no-store on metadata, offers, token responses, credential responses, and error responses that contain transaction information.

Credential configuration and data declarations

The app uses the offer to select the IDAK configuration and then resolves that configuration from issuer metadata. The issuer must therefore keep these values consistent:

credential_configuration_id in the offer
    -> fr.idak.mbicycle.1
    -> key in credential_configurations_supported

doctype in an mdoc Credential Request
    -> fr.idak.mbicycle.1
    -> the doctype in that configuration

The protocol steps are metadata-driven, but the current test credential schema is fixed. The mdoc namespace and element identifiers must belong to the declared fr.idak.mbicycle.1 schema and must be encoded with their actual CBOR types. A syntactically valid mdoc with the wrong doctype or unsupported data dictionary may still be rejected or may not be displayed by the wallet.

3. Create the Credential Offer

The recommended test invocation is an HTTPS credential_offer_uri:

openid-credential-offer://?credential_offer_uri=<URL-encoded-HTTPS-offer-URL>

The offer URL must return application/json and Cache-Control: no-store.

For the supported IDAK mdoc configuration, the response has this shape:

{
  "credential_issuer": "https://issuer.example.test/oid4vci/protocol/oid4vci/draft-13",
  "credential_configuration_ids": [
    "fr.idak.mbicycle.1"
  ],
  "grants": {
    "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
      "pre-authorized_code": "<single-use-code>",
      "user_pin_required": false
    }
  }
}

The pre-authorized code must be unpredictable, short-lived, single-use, and bound to the issuer and credential configuration. Never put a real code in documentation or logs.

If the offer contains a tx_code object, the wallet must provide the transaction code in the token request. Do not send a tx_code when the offer does not require one.

Keep the credential_issuer in the offer exactly equal to the value in issuer metadata. Do not put the final issuer URL in a Draft 13 offer.

4. Exchange the pre-authorized code for a token

The wallet posts the code to the metadata-advertised token_endpoint as an OAuth form request:

POST {token_endpoint} HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Accept: application/json

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code&pre-authorized_code=<URL-encoded-code>

A curl equivalent is:

curl --fail-with-body -X POST "$TOKEN_ENDPOINT" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Accept: application/json' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code' \
  --data-urlencode 'pre-authorized_code=<single-use-code>'

If required by the offer, add another --data-urlencode 'tx_code=<value>' field.

For the compatibility profile:

  • Do not send the token request as JSON.
  • Do not use Authorization: DPoP or add a DPoP proof.
  • Do not add client authentication when the metadata advertises token_endpoint_auth_method: ["none"].
  • Return ordinary Bearer access-token semantics. For strict Partenaires compatibility, use lowercase "bearer" for the JSON token_type value; do not return DPoP.

The successful response should include a nonce for the subsequent proof:

{
  "access_token": "<opaque-access-token>",
  "token_type": "bearer",
  "expires_in": 300,
  "c_nonce": "<fresh-transaction-nonce>",
  "c_nonce_expires_in": 300
}

The c_nonce must be generated for this issuance transaction and associated with the returned access token. The wallet must use that exact value in its next proof. Do not generate a nonce locally, reuse one from another issuer, or call a separate nonce endpoint.

5. Generate the Draft 13 proof JWT

Select the proof type and algorithm from the selected configuration, not from another issuer or another credential. For the currently tested Partenaires profile, the intersection is:

proof type:       jwt
proof algorithm:  ES256
holder key:       EC P-256
binding method:   cose_key for mdoc

The holder creates a fresh compact JWS for every Credential Request. Its protected header is:

{
  "typ": "openid4vci-proof+jwt",
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "<BASE64URL-X>",
    "y": "<BASE64URL-Y>"
  }
}

Its payload is:

{
  "aud": "https://issuer.example.test/oid4vci/protocol/oid4vci/draft-13",
  "iat": 1733659200,
  "nonce": "<c_nonce-from-this-token-response>"
}

The following checks are mandatory before sending the request:

  • typ is exactly openid4vci-proof+jwt.
  • alg is one of the algorithms advertised for the selected configuration; for the known-good profile it is ES256.
  • The JWK is public only: do not include the private d member.
  • kty is EC and crv is P-256 when ES256 is selected.
  • aud is exactly the metadata credential_issuer value.
  • nonce is the c_nonce from the current token response.
  • iat is close to the current Unix time.
  • The private key used to sign the JWS corresponds to the public x and y values in the header JWK.
  • Local signature verification with the header JWK succeeds before the HTTP request is sent.

The OID4VCI proof JWT is not a DPoP proof. Do not put it in an Authorization: DPoP header and do not use a DPoP nonce in its payload.

Draft 13 also defines a CWT proof type, but the Partenaires compatibility metadata described here advertises the JWT proof. Do not send a CWT unless the target wallet and the selected issuer configuration have been tested for that profile explicitly.

6. Send the Credential Request

The request is an HTTP POST to the credential_endpoint from metadata, with the access token as a Bearer token and a singular proof member.

IDAK mdoc request

POST {credential_endpoint} HTTP/1.1
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "format": "mso_mdoc",
  "doctype": "fr.idak.mbicycle.1",
  "proof": {
    "proof_type": "jwt",
    "jwt": "<signed-proof-jwt>"
  }
}

The doctype is fixed for this test path: it must be fr.idak.mbicycle.1. If claims is included, it must describe a subset of the IDAK mdoc data that the issuer can issue, and the resulting credential must contain the requested elements.

Do not send this Final 1.0-shaped body:

{
  "credential_configuration_id": "fr.idak.mbicycle.1",
  "proofs": {
    "jwt": [
      "<signed-proof-jwt>"
    ]
  }
}

Draft 13 uses proof, not proofs. Although the configuration ID and doctype have the same string value in this test profile, they are separate fields and must both be validated.

Unless metadata explicitly advertises response encryption and the wallet requests it, do not add a credential_response_encryption object or JWE processing to this compatibility flow.

7. Return a wallet-parseable credential

Return status 200 with Content-Type: application/json and Cache-Control: no-store.

The Draft 13 response uses the singular credential member:

{
  "credential": "<base64url-encoded-credential>",
  "notification_id": "<optional-notification-id>",
  "c_nonce": "<optional-fresh-nonce>",
  "c_nonce_expires_in": 300
}

For binary credentials, the credential value is a base64url string without padding. Do not serialize it as JSON, hex, a Buffer diagnostic object, or a base64 string nested inside another JSON object.

mdoc encoding requirements

For the Partenaires-compatible mdoc path, the decoded credential must be a real ISO/IEC 18013-5 CBOR Document container, not an empty diagnostic value and not only a server-side JSON representation of IssuerSigned.

At a structural level, the encoded document contains:

Document
โ”œโ”€โ”€ docType: text string
โ”œโ”€โ”€ issuerSigned
โ”‚   โ”œโ”€โ”€ nameSpaces: map(namespace -> array of IssuerSignedItemBytes)
โ”‚   โ””โ”€โ”€ issuerAuth: COSE_Sign1
โ””โ”€โ”€ deviceSigned
    โ”œโ”€โ”€ nameSpaces: DeviceNameSpacesBytes
    โ””โ”€โ”€ deviceAuth: holder-side device authentication structure

The issuer-signed portion must satisfy all of the following:

  • Document.docType equals the requested and advertised doctype.
  • Each issuer namespace contains IssuerSignedItemBytes, encoded as CBOR tag 24 around the byte string containing the encoded IssuerSignedItem.
  • Each item contains the correct digestID, a cryptographically random random value, the namespace element identifier, and the correctly typed elementValue.
  • The MSO valueDigests map contains the SHA-256 digest of the exact encoded IssuerSignedItemBytes, including the tag and byte-string wrapper.
  • The MSO docType, deviceKeyInfo.deviceKey, digest algorithm, and validity information are consistent with the document.
  • issuerAuth is a valid COSE Sign1 signed by the issuer key, and its certificate chain is the issuer chain trusted by the Partenaires test environment.
  • The public holder key in deviceKeyInfo.deviceKey corresponds to the key represented in the proof JWT. Do not generate one holder key, put another key in the proof, and bind a third key into the MSO.
  • Dates, booleans, numbers, arrays, byte strings, and images use the CBOR types required by the credential schema. Do not encode every value as a string.
  • Validity timestamps follow the ISO mdoc date/time rules; do not add fractional seconds where the selected profile forbids them.

The issuer does not create a fake presentation. The wallet supplies the holder/device authentication needed for a later presentation. However, the value returned at issuance must be the complete CBOR credential container accepted by the target wallet; an empty CBOR map, null, JSON {}, or a debugging object containing Buffer values is not a credential.

8. Implement the optional Notification Endpoint

If notification_endpoint is advertised and the response contains notification_id, the wallet may send:

POST {notification_endpoint} HTTP/1.1
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "notification_id": "<notification-id>",
  "event": "credential_accepted"
}

Supported event values are:

credential_accepted
credential_failure
credential_deleted

event_description is optional and must contain printable US-ASCII characters. The endpoint should be idempotent for a repeated identical notification and return a 2xx response; 204 No Content is appropriate.

The notification endpoint is protected by the same access-token transaction. It is not a replacement for the Credential Endpoint and it must not be used to deliver the credential.

9. Test with a raw HTTP trace before using the app

Use a redacted trace and check the following sequence:

  1. GET the profile-specific metadata URL and verify that it returns JSON and absolute endpoints.
  2. Open the HTTPS credential_offer_uri in the Partenaires app.
  3. Confirm that the offer's credential_configuration_ids contains a key present in metadata.
  4. POST the pre-authorized code as a form request to the advertised token endpoint.
  5. Confirm that the token response contains a fresh c_nonce.
  6. Generate a new proof JWT with aud == credential_issuer and nonce == c_nonce.
  7. POST the singular Draft 13 Credential Request with Authorization: Bearer.
  8. Decode the returned credential locally as base64url then CBOR; verify the mdoc structure, doctype, issuer signature, value digests, and holder-key binding.
  9. Check the credential in the Partenaires app and, if a notification was returned, record the notification response.

Never log pre-authorized codes, access tokens, private keys, full proof JWTs, or raw credentials. Log only redacted protocol facts such as HTTP status, content type, configuration ID, format, doctype, proof type, algorithm, nonce presence, and error codes.

Troubleshooting

Symptom Check first
token type ... is not supported Return "token_type": "bearer" and use Authorization: Bearer; do not switch to DPoP.
the field ... endpoint ... is not an URI Advertise a complete absolute https:// URI. Do not use /notification, an empty value, or a JSON object.
Wallet looks for a nonce endpoint Remove the Final 1.0 nonce exchange. Return c_nonce in the current token response and use it in the proof.
proofs rejected or proof missing Send one Draft 13 proof object with proof_type: "jwt".
invalid_proof Compare the proof aud, nonce, iat, typ, alg, JWK curve, and signature against the current transaction.
mdoc cannot be parsed or appears empty Decode the response as base64url CBOR, not JSON. Confirm a non-empty issuerSigned.nameSpaces, a valid issuerAuth, a matching MSO digest map, and the full document container.
Credential is issued but not shown Confirm the configuration ID and doctype are both fr.idak.mbicycle.1, and that the namespace, element identifiers, and CBOR data types match the supported IDAK mdoc schema.
Credential request reaches the wrong service Use the exact token_endpoint and credential_endpoint returned by metadata; do not derive URLs from the base issuer.
A retry behaves inconsistently Pre-authorized codes and access tokens are transaction state. Create a new offer and use a new proof for a new attempt.

When reporting an interoperability issue, retain the original HTTP status and JSON body, including error and error_description. Do not reduce an issuer response to a generic โ€œissuance failedโ€ message.

Partner acceptance checklist

Before asking for a Partenaires test, verify:

  • The Draft 13 issuer identifier is distinct from any final 1.0 issuer identifier.
  • /.well-known/openid-credential-issuer is appended to the Draft 13 issuer identifier.
  • Metadata is returned as application/json over HTTPS with Cache-Control: no-store.
  • All advertised endpoint values are absolute HTTPS URIs.
  • The offer contains only the supported test configuration fr.idak.mbicycle.1.
  • The metadata entry for fr.idak.mbicycle.1 declares format: "mso_mdoc" and doctype: "fr.idak.mbicycle.1".
  • The token request accepts application/x-www-form-urlencoded and the pre-authorized-code grant.
  • The token response returns ordinary Bearer semantics and a transaction-specific c_nonce.
  • The proof is a fresh, locally verifiable JWT with typ: openid4vci-proof+jwt.
  • The proof uses an algorithm and key curve advertised by the selected configuration.
  • The proof aud is the exact Draft 13 credential_issuer.
  • The proof nonce is the c_nonce from the same token response.
  • The Credential Request uses format: "mso_mdoc", doctype: "fr.idak.mbicycle.1", and singular proof.
  • The mdoc response is a non-empty, correctly encoded ISO CBOR credential with matching issuer signatures, digests, doctype, and holder key.
  • The response uses singular credential, not a final-profile response shape.
  • Any advertised notification endpoint accepts the returned access token and is idempotent.
  • The test trace contains no private keys, access tokens, pre-authorized codes, or full credentials.

Draft 13 references