You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

LTI 1.3 activityProgress更新401未授权问题排查(C#实现)

初始401错误的核心原因

你在发送activityProgress更新时,错误用了Launch流程返回的id_token作为Bearer令牌。LTI规范里,Launch的id_token仅用于工具自身的身份验证,不能用来调用平台的服务接口(比如分数/进度更新)。调用这类服务得走OAuth 2.0 Client Credentials流程:用工具私钥生成JWT断言,向平台申请专门的access_token。


获取AccessToken时500错误的排查点

从你补充的代码看,几个关键错误导致了内部服务器错误:

  1. JWT Payload字段大小写错误
    Payload里用了大写的Nbf,但JWT标准要求是小写的nbf,平台解析时会因识别不了字段报错。
  2. 时间字段格式错误
    iat、exp、nbf必须是Unix时间戳整数,你转成字符串了,这会让JWT断言格式无效,直接用EpochTime.GetIntDate()返回的整数就行。
  3. 多余请求头干扰
    你给HttpClient加了typ和alg请求头,这俩是JWT自身的Header字段,不用作为HTTP请求头发送,多余的头会打乱平台的请求解析。
  4. Audience字段配置错误
    aud字段应该是平台的Issuer URL(也就是Launch时id_token里的iss值),不是AccessTokenUrl。LTI的Client Credentials流程中,JWT断言的受众是平台的issuer,而非令牌端点。
  5. 参数名错误
    申请令牌的表单参数里,JWT断言对应的参数名是client_assertion,不是你写的assertion。

修正后的GetAccessTokenAsync代码示例

public async Task<TokenResponse> GetAccessTokenAsync(string scope, LtiConfiguration platform)
{
    if (string.IsNullOrEmpty(scope))
        throw new ArgumentNullException(nameof(scope));
    if (platform is null)
        throw new ArgumentNullException(nameof(platform));

    var clientId = _encryptionService.Decrypt(platform.ClientId);
    var issuer = _encryptionService.Decrypt(platform.Issuer);
    var tokenEndpoint = _encryptionService.Decrypt(platform.AccessTokenUrl);

    var payload = new JwtPayload
    {
        { "iss", clientId },
        { "sub", clientId },
        { "aud", issuer }, // 修正为平台的issuer
        { "iat", EpochTime.GetIntDate(DateTime.UtcNow) }, // 直接用整数时间戳
        { "exp", EpochTime.GetIntDate(DateTime.UtcNow.AddMinutes(5)) },
        { "nbf", EpochTime.GetIntDate(DateTime.UtcNow.AddSeconds(-5)) }, // 改为小写nbf
        { "jti", Guid.NewGuid().ToString() } // 用Guid生成jti更可靠
    };

    var handler = new JwtSecurityTokenHandler();
    var credentials = PemHelper.SigningCredentialsFromPemString(_encryptionService.Decrypt(platform.PrivateKey));
    var jwt = handler.WriteToken(new JwtSecurityToken(new JwtHeader(credentials), payload));

    using var httpClient = _clientFactory.CreateClient();
    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("client_id", clientId),
        new KeyValuePair<string, string>("grant_type", "client_credentials"),
        new KeyValuePair<string, string>("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
        new KeyValuePair<string, string>("scope", scope),
        new KeyValuePair<string, string>("client_assertion", jwt) // 修正参数名
    });

    var response = await httpClient.PostAsync(tokenEndpoint, content);

    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<TokenResponse>(responseContent);
    }
    else
    {
        // 捕获错误内容方便排查
        var errorContent = await response.Content.ReadAsStringAsync();
        throw new Exception($"获取AccessToken失败: {response.StatusCode} - {errorContent}");
    }
}

调用进度更新接口的修正

在UpdateLtiStatusAsync里,先获取合法的access_token再发起请求:

public async Task UpdateLtiStatusAsync(LtiStatusUpdateInputModel inputModel, LtiConfiguration platform)
{
    // 拼接所需权限范围,确保包含AGS服务的分数更新权限
    var scope = string.Join(" ", inputModel.EndpointClaim.Scope ?? new List<string>());
    var tokenResponse = await GetAccessTokenAsync(scope, platform);

    using var client = _clientFactory.CreateClient();
    var request = new HttpRequestMessage(HttpMethod.Post, inputModel.EndpointClaim.LineItemScoreEndpoint);
    request.Headers.Add("Authorization", $"Bearer {tokenResponse.AccessToken}");

    var payload = new
    {
        timestamp = inputModel.Timestamp.ToString("s"),
        userId = inputModel.UserId,
        activityProgress = inputModel.ActivityProgress.ToString(),
        gradingProgress = inputModel.GradingProgress.ToString()
    };

    var jsonPayload = JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/vnd.ims.lis.v1.score+json");

    request.Content = content;

    var response = await client.SendAsync(request);
    if (!response.IsSuccessStatusCode)
    {
        var errorContent = await response.Content.ReadAsStringAsync();
        throw new Exception($"更新LTI进度失败: {response.StatusCode} - {errorContent}");
    }
}

额外注意事项

  • 确保工具私钥和平台配置的公钥匹配,平台需要提前知晓工具的公钥(通过JWKS端点或直接配置)。
  • 检查scope是否正确,必须包含平台要求的权限(比如https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.score)。
  • 调试时一定要捕获并打印平台返回的错误内容,这是排查问题最直接的方式。

内容的提问来源于stack exchange,提问作者Pauldb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 15:59:54