[Bug] Calls to SharePoint Online REST API Fails with Invalid issuer or signature error
See original GitHub issueWhich version of Microsoft Identity Web are you using? Microsoft Identity Web 1.7.0
Where is the issue?
- Web app
- Sign-in users
- Sign-in users and call web APIs
- Web API
- Protected web APIs (validating tokens)
- Protected web APIs (validating scopes)
- Protected web APIs call downstream web APIs
- Token cache serialization
- In-memory caches
- Session caches
- Distributed caches
- Other (please describe)
Is this a new or an existing app? This is a new app or an experiment
Repro This involves onbehalfof (OBO) flow. So we need two apps.
- Client App
- Web API App
Provision Web API App
- Create Azure AD App registration (named: demo-webapi-spo)
- API Permissions:
- Go to ‘Expose an API’ and set app id uri and scope:
- Copy the scope uri and store it to use in Postman for generating the access token. Example scope uri: api://3fbcf6b8-baa1-4f47-a730-4f3c3e003b31/access_as_user
- Below two steps will need the Client APP Azure AD App ID. Finish ‘provision client app’ steps first and then follow below steps
- Add the AAD App ID corresponding to the Client App to the ‘Authorized client applications’ section. (see above image)
- Edit the manifest to add the AAD App ID of the Client App to the ‘KnownClientApplications’ section:
Provision Client App
- Create a new Azure AD App registration (named: client-app-spo)
- Set the redirect url to postman callback url (https://oauth.pstmn.io/v1/callback)
- API permissions - Add permissions to the demo-webapi-spo app for the access_as_user scope:
Admin consent Construct admin consent url that looks like this: https://login.microsoftonline.com/{Tenant-ID}/adminconsent?client_id={Client-App-AAD-App-ID} Access this url in browser and login using an admin account to consent on behalf of the org for all users. Note that the consent prompt in addition to the access_as_user scope for the web api, should also show the permissions for the SPO Rest API too (since client app is added as knownclientapp in the Web API App).
Create project for Web API App -Use dotnet new command to create a new project that calls SPO rest api. dotnet new webapi2 --auth SingleOrg --called-api-url https://contoso.sharepoint.com --client-id <WebAPI_APP_ID> --tenant-id <TENANT_ID> --domain Contoso.OnMicrosoft.com -o demo-webapi-spo -Ensure the appSettings.json file is updated with correct Client secret and also set the scope to AllSites.Read -The project has sample code auto-generated. I made a minor change by providing the ‘options’ to call a specific SPO site API.
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
using var response = await _downstreamWebApi.CallWebApiForUserAsync("DownstreamApi", options => {
options.RelativePath = $"sites/ModernTeamSite/_api/web/lists";
}).ConfigureAwait(false);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var apiResult = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// Do something
}
else
{
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}: {error}");
}
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
- Run dotnet build
- Run dotnet run
- You can access the API and call above method at https://localhost:5001/WeatherForecast (say in Postman client). It will give unauthorized error - which is expected.
Prepare Client App in Postman
- I used Postman client to generate access token to call the above api.
- Here is the config for the token generation
- Then used the acces token as bearer token to call the web api
- This returns the invalid issuer or signature error
- Here is the error:
System.Net.Http.HttpRequestException: Invalid status code in the HttpResponseMessage: Unauthorized: {"error_description":"Invalid issuer or signature."}
at demo_webapi_spo.Controllers.WeatherForecastController.Get() in C:\D\mg\demo-webapi-spo\Controllers\WeatherForecastController.cs:line 56
at lambda_method(Closure , Object )
at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Expected behavior Able to call the SPO REST API without any errors
Actual behavior Call to SPO REST API leads to this error: Unauthorized: {“error_description”:“Invalid issuer or signature.”}
Possible solution When I use ITokenAcquisiton implementation and use code _tokenAcquisition.GetAccessTokenForUserAsync() to call the same SPO REST API it works just fine. Here are the steps:
- Updated code to do dependency injection of ITokenAcquisition and HttpClient into our controller.
private readonly ITokenAcquisition _tokenAcquisition;
private readonly HttpClient _httpClient;
public WeatherForecastController(ILogger<WeatherForecastController> logger,
IDownstreamWebApi downstreamWebApi, ITokenAcquisition tokenAcquisition, HttpClient httpClient)
{
_logger = logger;
_downstreamWebApi = downstreamWebApi;
_tokenAcquisition = tokenAcquisition;
_httpClient = httpClient;
}
- Then updated the method to use HttpClient when if it fails using CallWebApiForUserAsync method
using var response = await _downstreamWebApi.CallWebApiForUserAsync("DownstreamApi", options => {
options.RelativePath = $"sites/ModernTeamSite/_api/web/lists";
}).ConfigureAwait(false);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
methodUsed = "[CallWebApiForUserAsync]";
// Do something with apiResult
}
else
{
var token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "https://contoso.sharepoint.com/AllSites.Read" });
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response2 = await _httpClient.GetAsync("https://contoso.sharepoint.com/sites/ModernTeamSite/_api/web/lists");
if(response2.IsSuccessStatusCode)
{
result = await response2.Content.ReadAsStringAsync().ConfigureAwait(false);
methodUsed = "[GetAccessTokenForUserAsync]";
}
else{
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}: {error}");
}
}
- Now when I call the API it successfully runs.
Additional context / logs / screenshots Add any other context about the problem here, such as logs and screenshots.
Issue Analytics
- State:
- Created 3 years ago
- Comments:8
That is it. That did it. When I worked with MS Graph the Scopes worked with just the scope name (without the graph url), hence I expected the same with SPO.
Thank you for your help @jmprieur. Appreciate your time. Please close this issue.
Thanks @svarukala In your appsettings.json, “Scopes” should be set to “https://contoso.sharepoint.com/AllSites.Read”