Home Supported Standards Terms & Conditions Privacy Policy Documentation Contact About
  1. 0. Choose a use caseHow the issuance starts
  2. 1. Discover the issuerIssuer + authorization server metadata
  3. 2. Authorize & approveAuthenticate, review tokens, request it
  4. 3. The credentialThe issued SD-JWT VC and its disclosures
  5. 4. Refresh itRefresh Token, then the issuer again

← Back to step 3 (the credential)

VC Issuance — Step 4 of 4: Refresh the Credential

A credential does not stay current: its claim values go out of date and its validity window runs out. OID4VCI section 14.5, Refreshing Issued Credentials, names two ways to deal with that. The first needs no End-User interaction at all: the wallet obtains a fresh Access Token with its Refresh Token (RFC 6749 section 6) and asks the Credential Endpoint again. The second is to start the issuance over from step 1, which does involve the End-User — and is the only route left once the Refresh Token has expired too. This page is the first: the two calls it is made of, and then what the issuer actually changed.

Tip: click a pane's title to collapse/expand it.

The Credential You Hold

What there is to refresh, and why anyone would: the validity window is the usual reason, but not the only one — a claim the issuer knows has changed is another, and the wallet cannot tell from the credential which of those applies.

 
What kind of credential this is — a vct for an SD-JWT VC, a type array for a W3C credential. A refresh asks for the same one — this is not a way to obtain a different credential.
The credential's validity window next to now — nbf and exp for a JWS-secured credential, validFrom and validUntil for a W3C one. An expired credential is the plainest case for refreshing; a valid one may still be refreshed (section 14.3).
The holder key this credential is bound to — cnf.jwk for an SD-JWT VC, credentialSubject.id (a did:jwk) for a W3C credential, which has no cnf claim. Refreshing it in place needs the private half of that key, because the Credential Request has to carry a key proof for it.
Which credential configuration was asked for, from which issuer, when — and how many times it has been refreshed already.
Credential History

Every attempt made on this page, newest first — not only the ones that worked. Each access token refresh, each Credential Request, each poll of the Deferred Credential Endpoint, and what you then decided about what came back, is one row with what the issuer actually said. (It is this workflow's counterpart of the Token History pane on oauth2_oidc_2.html, which does the same for token sets — and it records more, because “I tried to refresh and it did not work” is the case a debugger is most needed for.)

The # column numbers the attempts in the order they were made, so every row has one; Gen is the generation number, which only the rows the wallet holds have — those are the credentials there is something to go back to. The newest 100 attempts are kept, in a scrolling list of fixed height so the panes below it do not move as the log grows.

Attempt is what was tried; Outcome is what came of it: success (an access token came back), FAILED (with the refusal in the next column), deferred (the issuer needs more time), returned — not kept yet (a credential is in hand awaiting your decision), kept, or discarded. Only the kept rows are generations the wallet holds — they carry a Gen number and can be activated; everything else is there as a record and says log only.

Activate makes a generation the credential in hand again, together with the holder key it is bound to (without that key the credential could not be presented at all), so you can move backwards as well as forwards: a refresh that turned out worse than what it replaced is a real outcome, and going back to the credential that was working is what a wallet does about it.

 
 
Refresh the Access Token

Plain OAuth 2.0: grant_type=refresh_token at the authorization server's Token Endpoint. Nothing about this call is OID4VCI-specific — which is section 14.5's point. What is specific is what the token is for: it is about to be spent on the Credential Endpoint again. If the access token in hand has not expired, this step can be skipped entirely; section 14.3 permits asking the Credential Endpoint again with the token you already have.

token_endpoint from the authorization server metadata retrieved in step 1 — the same endpoint the OIDC leg used to exchange the authorization code.
The access token the workflow currently holds, with its own expiry — the thing this call is about to replace.
Issued alongside the access token, and the whole reason a credential can be refreshed without troubling the End-User. An authorization server is not obliged to issue one.

Inspect

OPTIONAL, and narrowing only: RFC 6749 section 6 allows a scope no broader than the one originally granted. Leave it empty to keep the scope the credential was authorized with.
The Token Request exactly as it will be sent: method, URL, and the form-encoded body. A confidential client's client_secret appears here too, because it is part of the call.
Exchange the refresh token for a fresh access token. No credential is issued by this call — that is the pane below.
 
The token endpoint's answer, verbatim. Watch whether a new refresh_token comes back: a rotating server invalidates the old one.
Ask the Credential Endpoint Again

The same Credential Request step 2 makes — a fresh c_nonce, a proof of possession signed by the holder key, the credential named the way section 8.2 requires — sent with the access token from above. Section 14.3 is explicit that the endpoint may be asked again, and equally explicit that the issuer decides what comes back: the same credential, or an updated one. It may also decide the access token is no longer good enough and answer 401, which is its right.

credential_endpoint from the credential issuer metadata retrieved in step 1.
The access token this request will carry as a Bearer credential — the refreshed one if the pane above produced one.
A refreshed credential normally stays bound to the key the old one used, which is what makes it a replacement. Binding a new key produces a credential a verifier will demand a different proof of possession for — a legitimate thing to want, but not a replacement.
 
A fresh nonce from the issuer's Nonce Endpoint. The one step 2 used was spent on the original issuance — a c_nonce is single use.
The CREDENTIAL's key proof, the same one step 2 sends: typ openid4vci-proof+jwt, signed by the holder key, naming the credential issuer as its audience and carrying the c_nonce above. It is what binds the refreshed credential to that key, and every Credential Request carries one with or without DPoP. It is NOT the DPoP proof, which is a typ dpop+jwt header.
The JSON that will be POSTed. Identical in shape to step 2's — a refresh is not a different request, it is the same request made again.
 
The fully assembled HTTP request: method, full URL, headers including the Bearer access token, and the body.
Send it. Nothing the wallet already holds is touched by this: what comes back is compared with the credential in hand, and replaces it only if you say so.
 
The Credential Response, verbatim.
Next: Present It

The presentation workflow is the other half of the format: a Verifier asks for some of this credential's claims over OID4VP, and this wallet answers with only the Disclosures it chooses plus a Key Binding JWT. Nothing is copied by going there — both workflows read the same browser storage, so what goes is whatever this wallet is holding: the generation marked as in hand above, with the holder key it is bound to.

 

VC Tools

Tools that are useful at any point in these workflows, and belong to none of their steps.

Resolve a DID to its document, read the keys it publishes, check that one of them really signed the credential this browser is holding, and check that the DID and an origin are the same entity.
Resolve a Decentralized Identifier (did:jwk, did:key, did:web) to its document, verify that a key it publishes signed the credential in this browser, and verify a domain linkage (DIF Well Known DID Configuration). Opens in this tab; the workflow's state is untouched.
Build a Root, Intermediate and Issuing CA, issue certificates with any X.509v3 extension, and test a real TLS or mutual-TLS handshake with what you issued.
Build a certificate authority — Root, Intermediate and Issuing — and issue TLS server, TLS client, code-signing and S/MIME certificates with full X.509v3 extension control. Useful when the issuer or verifier is behind TLS you control. Opens in this tab; the workflow's state is untouched.