使用 Azure DevOps OAuth 2.0 创建 Web 应用

Azure DevOps Services

重要

此信息仅适用于现有的 Azure DevOps OAuth 应用。 新应用开发人员应使用 Microsoft Entra ID OAuth 与 Azure DevOps 集成。

Azure DevOps 是 OAuth 2.0 应用的标识提供者。 OAuth 2.0 的实现可让开发人员为用户授权其应用,并获取 Azure DevOps 资源的访问令牌。

Azure DevOps OAuth 入门

1.注册应用

  1. 转到 https://app.vsaex.visualstudio.com/app/register 注册应用。

  2. 选择应用程序所需的范围,然后在授权应用使用相同的范围。 如果使用预览 API 注册了应用,请重新注册,因为所使用的范围现已弃用。

  3. 选择创建应用程序

    将显示应用程序设置页。

    Screenshot showing Applications settings for your app.

    • 当 Azure DevOps Services 向用户显示授权审批页时,它将使用公司名称、应用名称和说明。 它还使用公司网站、应用网站以及服务条款和隐私声明的 URL。

      Screenshot showing Visual Studio Codespaces authorization page with your company and app information.

    • 当 Azure DevOps Services 要求用户授权,并且用户授予授权时,用户的浏览器会使用授权代码重定向到授权回调 URL。 回调 URL 必须是安全连接(https),才能将代码传输回应用,并且与应用中注册的 URL 完全匹配。 如果没有,则会显示 400 错误页,而不是要求用户向应用授予授权的页面。

  4. 如果想要让用户授权应用访问其组织,请调用授权 URL 并传递应用 ID 和授权范围。 若要获取访问令牌来调用 Azure DevOps Services REST API,请调用访问令牌 URL。

注册的每个应用的设置可从配置文件 https://app.vssps.visualstudio.com/profile/view获取。

2.授权应用

  1. 如果用户未授权应用访问其组织,请调用授权 URL。 如果用户批准授权,它会使用授权代码调用你。
https://app.vssps.visualstudio.com/oauth2/authorize
        ?client_id={app ID}
        &response_type={Assertion}
        &state={state}
        &scope={scope}
        &redirect_uri={callback URL}
参数 类型 说明
client_id GUID 注册应用时分配给应用的 ID。
response_type string Assertion
state string 可以是任何值。 通常,生成的字符串值将回调与其关联的授权请求相关联。
scope string 向应用注册的范围。 空格分隔。 请参阅 可用范围
redirect_uri URL 应用的回调 URL。 必须与注册到应用的 URL 完全匹配。
  1. 将用户转到 Azure DevOps Services 授权终结点的站点添加链接或按钮:
https://app.vssps.visualstudio.com/oauth2/authorize
        ?client_id=88e2dd5f-4e34-45c6-a75d-524eb2a0399e
        &response_type=Assertion
        &state=User1
        &scope=vso.work%20vso.code_write
        &redirect_uri=https://fabrikam.azurewebsites.net/myapp/oauth-callback

Azure DevOps Services 要求用户授权应用。

假设用户接受,Azure DevOps Services 会将用户的浏览器重定向到回调 URL,包括短期授权代码和授权 URL 中提供的状态值:

https://fabrikam.azurewebsites.net/myapp/oauth-callback
        ?code={authorization code}
        &state=User1

3.获取用户的访问权限和刷新令牌

使用授权代码为用户请求访问令牌(和刷新令牌)。 服务必须向 Azure DevOps Services 发出服务到服务 HTTP 请求。

URL - 授权应用

POST https://app.vssps.visualstudio.com/oauth2/token

HTTP 请求标头 - 授权应用

标头
Content-Type application/x-www-form-urlencoded
Content-Type: application/x-www-form-urlencoded

HTTP 请求正文 - 授权应用

client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion={0}&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={1}&redirect_uri={2}

替换上一示例请求正文中的占位符值:

  • {0}:注册应用时获取的 URL 编码客户端密码
  • {1}:通过查询参数提供给回调 URL 的 code URL 编码的“代码”
  • {2}:向应用注册的回调 URL

用于形成请求正文的 C# 示例 - 授权应用

public string GenerateRequestPostData(string appSecret, string authCode, string callbackUrl)
{
   return String.Format("client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion={0}&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={1}&redirect_uri={2}",
               HttpUtility.UrlEncode(appSecret),
               HttpUtility.UrlEncode(authCode),
               callbackUrl
        );
}

响应 - 授权应用

{
    "access_token": { access token for the user },
    "token_type": { type of token },
    "expires_in": { time in seconds that the token remains valid },
    "refresh_token": { refresh token to use to acquire a new access token }
}

重要

安全地保留 refresh_token ,以便应用无需提示用户再次授权。 访问令牌快速过期,不应持久保存。

4.使用访问令牌

若要使用访问令牌,请将其作为持有者令牌包含在 HTTP 请求的授权标头中:

Authorization: Bearer {access_token}

例如,用于获取项目最近生成的 HTTP 请求

GET https://dev.azure.com/myaccount/myproject/_apis/build-release/builds?api-version=3.0
Authorization: Bearer {access_token}

5.刷新过期的访问令牌

如果用户的访问令牌过期,可以使用他们在授权流中获取的刷新令牌来获取新的访问令牌。 就像交换访问和刷新令牌的授权代码的原始过程一样。

URL - 刷新令牌

POST https://app.vssps.visualstudio.com/oauth2/token

HTTP 请求标头 - 刷新令牌

标头
Content-Type application/x-www-form-urlencoded
Content-Length 请求正文的计算字符串长度(请参阅以下示例)
Content-Type: application/x-www-form-urlencoded
Content-Length: 1654

HTTP 请求正文 - 刷新令牌

client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion={0}&grant_type=refresh_token&assertion={1}&redirect_uri={2}

替换上一示例请求正文中的占位符值:

  • {0}:注册应用时获取的 URL 编码客户端密码
  • {1}:用户的 URL 编码刷新令牌
  • {2}:向应用注册的回调 URL

响应 - 刷新令牌

{
    "access_token": { access token for this user },
    "token_type": { type of token },
    "expires_in": { time in seconds that the token remains valid },
    "refresh_token": { new refresh token to use when the token has timed out }
}

重要

为用户颁发新的刷新令牌。 保留此新令牌,并在下次需要为用户获取新的访问令牌时使用它。

示例

可以在 C# OAuth GitHub 示例中找到实现 OAuth 以调用 Azure DevOps Services REST API 的 C# 示例

重新生成客户端密码

每 5 年一次,应用程序机密将过期。 应重新生成应用机密,以便继续创建和使用访问令牌和刷新令牌。 为此,可以单击“重新生成机密”按钮,这将弹出一个对话框以确认要完成此操作。

Screenshot confirming secret regeneration.

当你确认要重新生成时,以前的应用机密将不再有效,并且使用此机密模拟的所有以前的令牌也将停止工作。 请确保将此客户端机密轮换时间很好地缩短,以最大程度地减少任何客户停机时间。

常见问题 (FAQ)

问:是否可以将 OAuth 与手机应用配合使用?

答:否。 Azure DevOps Services 仅支持 Web 服务器流,因此无法实现 OAuth,因为无法安全地存储应用机密。

问:代码中需要处理哪些错误或特殊条件?

答:确保处理以下条件:

  • 如果用户拒绝应用访问,则不会返回授权代码。 请勿在没有检查的情况下使用授权代码进行拒绝。
  • 如果用户撤销应用的授权,则访问令牌不再有效。 当应用使用令牌访问数据时,将返回 401 错误。 再次请求授权。

问:我想在本地调试 Web 应用。 注册应用时,是否可以将 localhost 用于回调 URL?

A:是的。 Azure DevOps Services 现在允许回调 URL 中的 localhost。 确保注册应用时用作 https://localhost 回调 URL 的开头。

问:尝试获取访问令牌时,我收到 HTTP 400 错误。 可能出了什么问题?

答:检查是否在请求标头中将内容类型设置为 application/x-www-form-urlencoded。

问:使用基于 OAuth 的访问令牌时,收到 HTTP 401 错误,但具有相同范围的 PAT 正常工作。 为什么?

答:验证组织管理员https://dev.azure.com/{your-org-name}/_settings/organizationPolicy未禁用通过 OAuth 进行第三方应用程序访问。 在此方案中,授权应用并生成访问令牌的流有效,但所有 REST API 仅返回错误,例如 TF400813: The user "<GUID>" is not authorized to access this resource.

问:是否可以将 OAuth 与 SOAP 终结点和 REST API 配合使用?

答:否。 目前仅在 REST API 中支持 OAuth。

问:如何使用 Azure DevOps REST API 获取工作项的附件详细信息?

答:首先,使用 工作项获取工作项详细信息 - 获取工作项 REST API:

GET https://dev.azure.com/{organization}/{project}/_apis/wit/workitems/{id}

若要获取附件详细信息,需要将以下参数添加到 URL:

$expand=all

使用结果可获取关系属性。 可在其中找到附件 URL,并在 URL 中查找 ID。 例如:

$url = https://dev.azure.com/{organization}/{project}/_apis/wit/workitems/434?$expand=all&api-version=5.0

$workItem = Invoke-RestMethod -Uri $url -Method Get -ContentType application/json

$split = ($workitem.relations.url).Split('/')

$attachmentId = $split[$split.count - 1]

# Result: 1244nhsfs-ff3f-25gg-j64t-fahs23vfs