Microsoft Entra 验证 ID 包括请求服务 REST API。 使用此 API 可以颁发和验证凭据。 本问将介绍如何开始使用请求服务 REST API。

API 访问令牌

应用程序需要包含具有所需权限的有效访问令牌,以便它可以访问请求服务 REST API。 Microsoft 标识平台发布的访问令牌包含请求服务 REST API 用于验证调用方的信息(范围)。 访问令牌确保调用方具有执行所请求的操作的适当权限。

若要获取访问令牌,你的应用必须向 Microsoft 标识平台注册,并由管理员授权以访问请求服务 REST API。 若尚未注册 verifiable-credentials-app 应用程序,请依次参阅 如何注册应用 生成应用程序机密

获取访问令牌

使用 OAuth 2.0 客户端凭据授予流 通过 Microsoft 标识平台获取访问令牌。 为此,请使用受信任的库。 在此教程中,我们使用 Microsoft 身份验证库 ( MSAL )。 MSAL 简化了向可调用安全 Web API 的应用添加身份验证和授权的过程。

Node.js Python
POST /{tenant}/oauth2/v2.0/token HTTP/1.1           //Line breaks for clarity
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&scope=3db474b9-6a0c-4840-96ac-1fceb342124f/.default
&client_secret=sampleCredentia1s
&grant_type=client_credentials
// Initialize MSAL library by using the following code
ConfidentialClientApplicationBuilder.Create(AppSettings.ClientId)
    .WithClientSecret(AppSettings.ClientSecret)
    .WithAuthority(new Uri(AppSettings.Authority))
    .Build();
// Acquire an access token
result = await app.AcquireTokenForClient(AppSettings.Scopes)
                .ExecuteAsync();
  auth: {
      clientId: config.azClientId,
      authority: `https://login.microsoftonline.com/${config.azTenantId}`,
      clientSecret: config.azClientSecret,
  system: {
      loggerOptions: {
          loggerCallback(loglevel, message, containsPii) {
              console.log(message);
          piiLoggingEnabled: false,
          logLevel: msal.LogLevel.Verbose,
const cca = new msal.ConfidentialClientApplication(msalConfig);
const msalClientCredentialRequest = {
  scopes: ["3db474b9-6a0c-4840-96ac-1fceb342124f/.default"],
  skipCache: false, 
module.exports.msalCca = cca;
module.exports.msalClientCredentialRequest = msalClientCredentialRequest;
// Acquire an access token
const result = await mainApp.msalCca.acquireTokenByClientCredential(mainApp.msalClientCredentialRequest);
    if ( result ) {
      accessToken = result.accessToken;
# Initialize MSAL library by using the following code
msalCca = msal.ConfidentialClientApplication( config["azClientId"], 
    authority="https://login.microsoftonline.com/" + config["azTenantId"],
    client_credential=config["azClientSecret"],
# Acquire an access token
accessToken = ""
result = msalCca.acquire_token_for_client( scopes="3db474b9-6a0c-4840-96ac-1fceb342124f/.default" )
if "access_token" in result:
    accessToken = result['access_token']
// Initialize MSAL library by using the following code
ConfidentialClientApplication app = ConfidentialClientApplication.builder(
                clientId,
                ClientCredentialFactory.createFromSecret(clientSecret))
                .authority(authority)
                .build();
// Acquire an access token
ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder(
                Collections.singleton(scope))
                .build();
CompletableFuture<IAuthenticationResult> future = app.acquireToken(clientCredentialParam);
IAuthenticationResult result = future.get();
return result.accessToken();
应用程序计划对其进行操作的目录租户。 例如:https://login.microsoftonline.com/{your-tenant}。 (使用租户 ID 或名称替换 your-tenant。)
客户端 ID
分配给应用的应用程序 ID。 可以在注册应用的 Azure 门户中找到此信息。
客户端机密
为应用生成的客户端密码。
必须设置为 3db474b9-6a0c-4840-96ac-1fceb342124f/.default。 此设置将生成一个角色声明为 VerifiableCredential.Create.All 的访问令牌。

有关如何使用控制台应用的标识获取访问令牌的详细信息,请参阅这些文章之一:

  • Python
  • Node.js
  • 还可以使用证书访问令牌请求(而不是使用客户端密码)。

    Node.js Python
    POST /{tenant}/oauth2/v2.0/token HTTP/1.1   //Line breaks for clarity
    Host: login.microsoftonline.com
    Content-Type: application/x-www-form-urlencoded
    client_id=12345678-0000-0000-00000000000000000
    &scope=3db474b9-6a0c-4840-96ac-1fceb342124f/.default
    &client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
    &client_assertion=eyJhbGciOiJSUzI1NiIsIng1dCI6Imd4OHRHeXN5amNScUtqRlBuZDdSRnd2d1pJMCJ9.eyJ{a lot of characters here}M8U3bSUKKJDEg
    &grant_type=client_credentials
    
    // Initialize MSAL library by using the following code
    X509Certificate2 certificate = AppSettings.ReadCertificate(AppSettings.CertificateName);
        app = ConfidentialClientApplicationBuilder.Create(AppSettings.ClientId)
            .WithCertificate(certificate)
            .WithAuthority(new Uri(AppSettings.Authority))
            .Build();
    // Acquire an access token
    result = await app.AcquireTokenForClient(AppSettings.Scopes)
                    .ExecuteAsync();
      auth: {
          clientId: config.azClientId,
          authority: `https://login.microsoftonline.com/${config.azTenantId}`,
          clientCertificate: {
                thumbprint: "CERT_THUMBPRINT", // a 40-digit hexadecimal string
                privateKey: "CERT_PRIVATE_KEY"
      system: {
          loggerOptions: {
              loggerCallback(loglevel, message, containsPii) {
                  console.log(message);
              piiLoggingEnabled: false,
              logLevel: msal.LogLevel.Verbose,
    const cca = new msal.ConfidentialClientApplication(msalConfig);
    const msalClientCredentialRequest = {
      scopes: ["3db474b9-6a0c-4840-96ac-1fceb342124f/.default"],
      skipCache: false, 
    module.exports.msalCca = cca;
    module.exports.msalClientCredentialRequest = msalClientCredentialRequest;
    // Acquire an access token
    const result = await mainApp.msalCca.acquireTokenByClientCredential(mainApp.msalClientCredentialRequest);
        if ( result ) {
          accessToken = result.accessToken;
    
    # Initialize MSAL library by using the following code
    with open(config["azCertificatePrivateKeyLocation"], "rb") as file:
        private_key = file.read()
    with open(config["azCertificateLocation"]) as file:
        public_certificate = file.read()
    cert = load_pem_x509_certificate(data=bytes(public_certificate, 'UTF-8'), backend=default_backend())
    thumbprint = (cert.fingerprint(hashes.SHA1()).hex())
    msalCca = msal.ConfidentialClientApplication( config["azClientId"], 
        authority="https://login.microsoftonline.com/" + config["azTenantId"],
        client_credential={
            "private_key": private_key,
            "thumbprint": thumbprint,
            "public_certificate": public_certificate
    # Acquire an access token
    accessToken = ""
    result = msalCca.acquire_token_for_client( scopes="3db474b9-6a0c-4840-96ac-1fceb342124f/.default" )
    if "access_token" in result:
        accessToken = result['access_token']
    
    // Initialize MSAL library by using the following code
    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Files.readAllBytes(Paths.get(certKeyLocation)));
    PrivateKey key = KeyFactory.getInstance("RSA").generatePrivate(spec);
    java.io.InputStream certStream = (java.io.InputStream)new ByteArrayInputStream(Files.readAllBytes(Paths.get(certLocation)));
    X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certStream);
    ConfidentialClientApplication app = ConfidentialClientApplication.builder(
                    clientId,
                    ClientCredentialFactory.createFromCertificate(key, cert))
                    .authority(authority)
                    .build();
    // Acquire an access token
    ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder(
                    Collections.singleton(scope))
                    .build();
    CompletableFuture<IAuthenticationResult> future = app.acquireToken(clientCredentialParam);
    IAuthenticationResult result = future.get();
    return result.accessToken();
    

    调用 API

    发布或验证可验证凭据:

  • 构建对请求服务 REST API 的 HTTP POST 请求。 URL 中不再需要租户 ID,因为它在访问令牌中显示为声明。

    POST https://verifiedid.did.msidentity.com/v1.0/verifiableCredentials/createIssuanceRequest
    
    POST https://verifiedid.did.msidentity.com/v1.0/verifiableCredentials/createPresentationRequest
    
  • 将访问令牌作为持有者令牌附加到 HTTP 请求中的授权标头。

    Authorization: Bearer <token>
    
  • Content-Type 标头设置为 Application/json

  • 准备好将发布出示请求的有效负载附加到请求正文。

  • 将请求提交到请求服务 REST API。

    请求服务 API 在成功调用后将返回 HTTP 状态代码 201 Created。 如果 API 调用返回错误,请检查错误参考文档

    发布请求示例

    下面的示例演示了可验证凭据的发布请求。 有关有效负载的信息,请参阅请求服务 REST API 发布规范

    POST https://verifiedid.did.msidentity.com/v1.0/verifiableCredentials/createIssuanceRequest
    Content-Type: application/json
    Authorization: Bearer  <token>
    {...JSON payload...}
        "includeQRCode": false,
        "callback": {
            "url": "https://contoso.com/api/issuer/issuanceCallback",
            "state": "de19cb6b-36c1-45fe-9409-909a51292a9c",
            "headers": {
                "api-key": "OPTIONAL API-KEY for CALLBACK EVENTS"
        "authority": "did:web:verifiedid.contoso.com",
        "registration": {
            "clientName": "Verifiable Credential Expert Sample"
        "type": "VerifiedCredentialExpert",
        "manifestUrl": "https://verifiedid.did.msidentity.com/v1.0/12345678-0000-0000-0000-000000000000/verifiableCredentials/contracts/VerifiedCredentialExpert1",
        "pin": {
            "value": "3539",
            "length": 4
        "claims": {
            "given_name": "Megan",
            "family_name": "Bowen"
    
        "includeQRCode": false,
        "callback": {
            "url": "https://contoso.com/api/issuer/issuanceCallback",
            "state": "de19cb6b-36c1-45fe-9409-909a51292a9c",
            "headers": {
                "api-key": "OPTIONAL API-KEY for CALLBACK EVENTS"
        "authority": "did:web:verifiedid.contoso.com",
        "registration": {
            "clientName": "Verifiable Credential Expert Sample"
        "type": "VerifiedCredentialExpert",
        "manifestUrl": "https://verifiedid.did.msidentity.com/v1.0/12345678-0000-0000-0000-000000000000/verifiableCredentials/contracts/VerifiedCredentialExpert1",
        "pin": {
            "value": "3539",
            "length": 4
        "claims": {
            "given_name": "Megan",
            "family_name": "Bowen"
        "expirationDate": "2024-12-31T23:59:59.000Z"
    

    出示请求示例

    下面的示例演示了可验证凭据的出示请求。 有关有效负载的信息,请参阅请求服务 REST API 出示规范

    POST https://verifiedid.did.msidentity.com/v1.0/verifiableCredentials/createPresentationRequest
    Content-Type: application/json
    Authorization: Bearer  <token>
    {...JSON payload...}
      "includeQRCode": true,
      "callback": {
        "url": "https://contoso.com/api/verifier/presentationCallback",
        "state": "92d076dd-450a-4247-aa5b-d2e75a1a5d58",
        "headers": {
          "api-key": "OPTIONAL API-KEY for CALLBACK EVENTS"
      "authority": "did:web:verifiedid.contoso.com",
      "registration": {
        "clientName": "Veritable Credential Expert Verifier"
      "includeReceipt": true,
      "requestedCredentials": [
          "type": "VerifiedCredentialExpert",
          "purpose": "So we can see that you a veritable credentials expert",
          "acceptedIssuers": [
            "did:web:verifiedid.contoso.com"
          "configuration": {
            "validation": {
              "allowRevoked": true,
              "validateLinkedDomain": true
      "authority": "did:web:verifiedid.contoso.com",
      "includeQRCode": false,
      "includeReceipt": false,
      "registration": {
        "clientName": "Contoso Job Application Center",
        "purpose": "Provide proof of attended courses"
      "callback": {
        "url": "https://contoso.com/api/verifier/presentationCallback",
        "state": "92d076dd-450a-4247-aa5b-d2e75a1a5d58",
        "headers": {
          "api-key": "OPTIONAL API-KEY for CALLBACK EVENTS"
      "requestedCredentials": [
          "type": "FabrikamCourseCertification",
          "acceptedIssuers": [ "did:web:learn.fabrikam.com" ],
          "constraints": [ 
              "claimName": "courseCode",
              "values": ["FC100", "FC110", "FC150"], 
              "claimName": "courseTitle",
              "contains": "network", 
          "configuration": {
            "validation": {
              "allowRevoked": false,
              "validateLinkedDomain": true
      "authority": "did:web:verifiedid.contoso.com",
      "includeQRCode": false,
      "includeReceipt": false,
      "registration": {
        "clientName": "Contoso Job Application Center",
        "purpose": "Provide proof of attended courses"
      "callback": {
        "url": "https://contoso.com/api/verifier/presentationCallback",
        "state": "92d076dd-450a-4247-aa5b-d2e75a1a5d58",
        "headers": {
          "api-key": "OPTIONAL API-KEY for CALLBACK EVENTS"
      "requestedCredentials": [
          "type": "VerifiedEmployee",
          "acceptedIssuers": [ "did:web:learn.contoso.com" ],
          "configuration": {
            "validation": {
              "allowRevoked": false,
              "validateLinkedDomain": true,
              "faceCheck": {
                "sourcePhotoClaimName": "photo",
                "matchConfidenceThreshold": 70
    

    请求负载包含发布出示回调终结点。 终结点是 Web 应用程序的一部分,应该可以通过 HTTPS 协议公开访问。 请求服务 API 会调用你的终结点,从而将特定事件通知给你的应用。 例如,这类事件可能是用户扫描 QR 码、在验证器应用中使用深层链接或完成出示过程。

    下方示意图描述了应用对请求服务 REST API 进行的调用,以及对应用程序进行的回调。

    配置终结点以侦听传入的 HTTP POST 请求。 下面的代码片段演示如何处理发布回调 HTTP 请求以及如何相应更新 UI:

    Node.js Python string content = new System.IO.StreamReader(this.Request.Body).ReadToEndAsync().Result; _log.LogTrace("callback!: " + content); JObject issuanceResponse = JObject.Parse(content); // More code here if (issuanceResponse["code"].ToString() == "request_retrieved") var cacheData = new status = "request_retrieved", message = "QR Code is scanned. Waiting for issuance...", _cache.Set(state, JsonConvert.SerializeObject(cacheData)); // More code here

    有关完整代码,请参阅 GitHub 存储库上的发布出示代码。

    mainApp.app.post('/api/issuer/issuance-request-callback', parser, async (req, res) => {
      var body = '';
      req.on('data', function (data) {
        body += data;
      req.on('end', function () {
        requestTrace( req );
        console.log( body );
        var issuanceResponse = JSON.parse(body.toString());
        var message = null;
        if ( issuanceResponse.code == "request_retrieved" ) {
          message = "QR Code is scanned. Waiting for issuance to complete...";
        if ( issuanceResponse.code == "issuance_successful" ) {
          message = "Credential successfully issued";
        if ( issuanceResponse.code == "issuance_error" ) {
          message = issuanceResponse.error.message;
        // More code here
        res.send()
      res.send()
    
    @app.route("/api/issuer/issuance-request-callback", methods = ['POST'])
    def issuanceRequestApiCallback():
        if request.headers['api-key'] != apiKey:
            return Response( jsonify({'error':'api-key wrong or missing'}), status=401, mimetype='application/json')
        issuanceResponse = request.json
        if issuanceResponse["code"] == "request_retrieved":
            cacheData = {
                "status": issuanceResponse["code"],
                "message": "QR Code is scanned. Waiting for issuance to complete..."
            cache.set( issuanceResponse["state"], json.dumps(cacheData) )
            return ""
        if issuanceResponse["code"] == "issuance_successful":
            cacheData = {
                "status": issuanceResponse["code"],
                "message": "Credential successfully issued"
            cache.set( issuanceResponse["state"], json.dumps(cacheData) )
            return ""
        if issuanceResponse["code"] == "issuance_error":
            cacheData = {
                "status": issuanceResponse["code"],
                "message": issuanceResponse["error"]["message"]
            cache.set( issuanceResponse["state"], json.dumps(cacheData) )
            return ""
        return ""
    
    @RequestMapping(value = "/api/issuer/issue-request-callback", method = RequestMethod.POST, produces = "application/json", consumes = "application/json")
        public ResponseEntity<String> issueRequestCallback( HttpServletRequest request
                                                          , @RequestHeader HttpHeaders headers
                                                          , @RequestBody String body ) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                if ( !request.getHeader("api-key").equals(apiKey) ) {
                    lgr.info( "api-key wrong or missing" );
                    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( "api-key wrong or missing" );
                JsonNode presentationResponse = objectMapper.readTree( body );
                String code = presentationResponse.path("code").asText();
                ObjectNode data = null;
                if ( code.equals( "request_retrieved" )  ) {
                    data = objectMapper.createObjectNode();
                    data.put("message", "QR Code is scanned. Waiting for issuance to complete..." );
                if ( code.equals("issuance_successful") ) {
                    data = objectMapper.createObjectNode();
                    data.put("message", "Credential successfully issued" );
                if ( code.equals( "issuance_error" ) ) {
                    data = objectMapper.createObjectNode();
                    data.put("message", presentationResponse.path("error").path("message").asText() );
                if ( data != null ) {
                    data.put("status", code );
                    cache.put( presentationResponse.path("state").asText(), objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data) );
            } catch (java.io.IOException ex) {
                ex.printStackTrace();
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body( "Technical error" );
            return ResponseEntity.ok().body( "{}" );
    

    有关完整代码,请参阅 GitHub 存储库上的发布出示代码。

    详细了解以下规范:

  • 颁发 API 规范
  • 演示 API 规范
  •