Creating a new Blazor WebAssembly App with Microsoft Visual Studio 2019 Version 16.9.4 with these specifications: Target Framework .NET 5.0, Authentication Type Individual Accounts and ASP.NET Core Hosted:
Gives a Server project with these NuGets at version 5.0.5:
Microsoft.AspNetCore.ApiAuthorization.IdentityServer
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.AspNetCore.Identity.UI
Startup.cs contains this code:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
Reading the blog post ASP.NET Core Authentication with IdentityServer4 from Microsoft I should be able to retrieve a token with a sample request that looks like this:
POST /connect/token HTTP/1.1
Host: localhost:5000
Cache-Control: no-cache
Postman-Token: 958df72b-663c-5638-052a-aed41ba0dbd1
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=Mike%40Contoso.com&password=MikesPassword1!&client_id=myClient&scope=myAPIs
https://devblogs.microsoft.com/aspnet/asp-net-core-authentication-with-identityserver4/
Creating a request that looks like that but for the solution created:
POST /connect/token HTTP/1.1
Host: localhost:44388
Content-Type: application/x-www-form-urlencoded
Content-Length: 153
grant_type=password&username=example%40example.com&password=Password1&client_id=WebApplication4.Client&scope=WebApplication4.ServerAPI%20openid%20profile
This request returns HTTP Status 400 Bad Request with body:
{
"error": "unauthorized_client"
}
I'm pretty sure the values are correct since I got client_id and scope from the request used to sign in to the web application. That flow does not use grant_type=password though. Example request from login:
https://localhost:44388/Identity/Account/Login?ReturnUrl=/connect/authorize/callback?client_id=WebApplication4.Client&redirect_uri=https%3A%2F%2Flocalhost%3A44388%2Fauthentication%2Flogin-callback&response_type=code&scope=WebApplication4.ServerAPI%20openid%20profile&state=12345&code_challenge=12345&code_challenge_method=S256&response_mode=query
Confirmation that the user exists and works:
What am I missing?
TLDR:
Remove this from appsettings.json:
"Clients": {
"WebApplication4.Client": {
"Profile": "IdentityServerSPA"
}
}
Edit Startup.cs:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.Clients.AddIdentityServerSPA("WebApplication4.Client", builder =>
{
builder.WithRedirectUri("/authentication/login-callback");
builder.WithLogoutRedirectUri("/authentication/logout-callback");
});
//Or Duende.IdentityServer.Models.Client
options.Clients.Add(new IdentityServer4.Models.Client
{
ClientId = "WebApplication4.Integration",
AllowedGrantTypes = { GrantType.ResourceOwnerPassword },
//Use Configuration.GetSection("MySecretValue").Value; to get a value from appsettings.json
ClientSecrets = { new Secret("MySecretValue".Sha256()) },
AllowedScopes = { "WebApplication4.ServerAPI", "openid", "profile" }
});
});
This request will work:
POST /connect/token HTTP/1.1
Host: localhost:44388
Content-Type: application/x-www-form-urlencoded
Content-Length: 168
grant_type=password&username=example%40example.com&password=Password1!&client_id=WebApplication4.Integration&scope=WebApplication4.ServerAPI&client_secret=MySecretValue
Long answer:
I started out with trying to get a better error messages with Logging.
I added the code below to public static IHostBuilder CreateHostBuilder(string[] args) in
Program.cs:
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
})
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0
When debugging I could then show output from the Server application when I made the request. It looked like this:
info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.TokenEndpoint for /connect/token
info: IdentityServer4.Events.DefaultEventService[0]
{
"ClientId": "WebApplication4.Client",
"AuthenticationMethod": "NoSecret",
"Category": "Authentication",
"Name": "Client Authentication Success",
"EventType": "Success",
"Id": 1010,
"ActivityId": "8000000a-0000-8f00-b63f-84710c7967bb",
"TimeStamp": "2021-04-29T11:47:07Z",
"ProcessId": 8436,
"LocalIpAddress": "::1:44388",
"RemoteIpAddress": "::1"
}
fail: IdentityServer4.Validation.TokenRequestValidator[0]
Client not authorized for resource owner flow, check the AllowedGrantTypes setting{ client_id = WebApplication4.Client }, details: {
"ClientId": "WebApplication4.Client",
"ClientName": "WebApplication4.Client",
"GrantType": "password",
"Raw": {
"grant_type": "password",
"username": "example#example.com",
"password": "***REDACTED***",
"client_id": "WebApplication4.Client",
"scope": "WebApplication4.ServerAPI"
}
}
info: IdentityServer4.Events.DefaultEventService[0]
{
"ClientId": "WebApplication4.Client",
"ClientName": "WebApplication4.Client",
"Endpoint": "Token",
"GrantType": "password",
"Error": "unauthorized_client",
"Category": "Token",
"Name": "Token Issued Failure",
"EventType": "Failure",
"Id": 2001,
"ActivityId": "8000000a-0000-8f00-b63f-84710c7967bb",
"TimeStamp": "2021-04-29T11:47:07Z",
"ProcessId": 8436,
"LocalIpAddress": "::1:44388",
"RemoteIpAddress": "::1"
}
The error message to look at is Client not authorized for resource owner flow, check the AllowedGrantTypes setting{ client_id = WebApplication4.Client }.
With this error message I found this Question:
Question about ASP.NET Core 3 Identity / Identity Server / SPA support for Resource Owner Password Grant Type
There I could read
found that the allowed grant type of password was not being added when
the profile is set to IdentityServerSPA.
Looking at appsettings.json the application uses that profile:
"IdentityServer": {
"Clients": {
"WebApplication4.Client": {
"Profile": "IdentityServerSPA"
}
}
},
Looking at Microsoft Application profiles what it actually does is this:
The redirect_uri defaults to /authentication/login-callback.
The post_logout_redirect_uri defaults to
/authentication/logout-callback.
The set of scopes includes the openid, profile, and every scope
defined for the APIs in the app.
The set of allowed OIDC response types is id_token token or each of
them individually (id_token, token).
The allowed response mode is fragment.
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-5.0#application-profiles
Before starting to modify this I visited the URL https://localhost:44388/.well-known/openid-configuration to get the current configuration. It looked like this and specifically says grant_types_supported: ...password:
{
"issuer": "https://localhost:44388",
"jwks_uri": "https://localhost:44388/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://localhost:44388/connect/authorize",
"token_endpoint": "https://localhost:44388/connect/token",
"userinfo_endpoint": "https://localhost:44388/connect/userinfo",
"end_session_endpoint": "https://localhost:44388/connect/endsession",
"check_session_iframe": "https://localhost:44388/connect/checksession",
"revocation_endpoint": "https://localhost:44388/connect/revocation",
"introspection_endpoint": "https://localhost:44388/connect/introspect",
"device_authorization_endpoint": "https://localhost:44388/connect/deviceauthorization",
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true,
"scopes_supported": [
"openid",
"profile",
"WebApplication4.ServerAPI",
"offline_access"
],
"claims_supported": [
"sub",
"name",
"family_name",
"given_name",
"middle_name",
"nickname",
"preferred_username",
"profile",
"picture",
"website",
"gender",
"birthdate",
"zoneinfo",
"locale",
"updated_at"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"implicit",
"password",
"urn:ietf:params:oauth:grant-type:device_code"
],
"response_types_supported": [
"code",
"token",
"id_token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"response_modes_supported": [
"form_post",
"query",
"fragment"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"subject_types_supported": [
"public"
],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"request_parameter_supported": true
}
For some reason IdentityServer Clients can not be configured in code and in appsettings.json. I therefore removed Clients from appsettings.json and added this to Startup.cs:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.Clients.AddIdentityServerSPA("WebApplication4.Client", builder =>
{
builder.WithRedirectUri("/authentication/login-callback");
builder.WithLogoutRedirectUri("/authentication/logout-callback");
});
options.Clients.Add(new IdentityServer4.Models.Client
{
ClientId = "WebApplication4.Integration",
AllowedGrantTypes = { GrantType.ResourceOwnerPassword },
AllowedScopes = { "WebApplication4.ServerAPI", "openid", "profile" }
});
});
Without WithRedirectUri and WithLogoutRedirectUri it did not work, OidcConfigurationController got an exception for ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId); with System.InvalidOperationException: 'Sequence contains no elements'. For some reason this is fixed automatically when using appsettings.json.
I now got the error message when posting to /connect/token:
{
"error": "invalid_client"
}
But I got a much better error in the log:
Invalid client configuration for client WebApplication4.Integration: Client secret is required for password, but no client secret is configured.
Added a secret to Startup.cs:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.Clients.AddIdentityServerSPA("WebApplication4.Client", builder =>
{
builder.WithRedirectUri("/authentication/login-callback");
builder.WithLogoutRedirectUri("/authentication/logout-callback");
});
options.Clients.Add(new IdentityServer4.Models.Client
{
ClientId = "WebApplication4.Integration",
AllowedGrantTypes = { GrantType.ResourceOwnerPassword },
//Use Configuration.GetSection("MySecretValue").Value; to get a value from appsettings.json
ClientSecrets = { new Secret("MySecretValue".Sha256()) },
AllowedScopes = { "WebApplication4.ServerAPI", "openid", "profile" }
});
});
And the request:
POST /connect/token HTTP/1.1
Host: localhost:44388
Content-Type: application/x-www-form-urlencoded
Content-Length: 168
grant_type=password&username=example%40example.com&password=Password1!&client_id=WebApplication4.Integration&scope=WebApplication4.ServerAPI&client_secret=MySecretValue
It finally worked and the normal login flow worked as well!
I am using RestSharp to push an invoice to MYOB.
RestClient myobPostInvoicesClient = new RestClient("https://api.myob.com/");
RestRequest myobPostInvoicesRequest = new RestRequest("au/essentials/businesses/" + business_uid + "/sale/invoices", Method.POST);
myobPostInvoicesRequest.AddHeader("Authorization", "Bearer " + access_token);
myobPostInvoicesRequest.AddHeader("x-myobapi-key", clientId);
myobPostInvoicesRequest.AddHeader("x-myobapi-version", "v0");
myobPostInvoicesRequest.AddHeader("Content-Type", "application/json");
The JSON I am sending to the endpoint is as below
{{
"contact": {
"uid": "26939970"
},
"invoiceNumber": "IV00000000082",
"issueDate": "2020-06-07T09:00:00",
"dueDate": "2020-07-07T09:00:00",
"gstInclusive": "true",
"status": "Open",
"lines": [
{
"unitOfMeasure": "Qty",
"quantity": 5.0,
"unitPrice": 1000.0,
"total": 5000.0,
"taxType": {
"uid": "10"
},
"account": {
"uid": "9"
},
"description": "Test Description"
}
]
}}
The Response I am getting back from the MYOB Invoice API endpoint is
"{\"errors\":[{\"field\":\"\",\"message\":\"Forbidden\",\"code\":\"403\"}]}"
The access token and client id are both valid and I am following the structure of the Invoice based on the below link
https://developer.myob.com/api/essentials-accounting/endpoints/sale/invoices/
The ones I have included in the request where the fields that were previously marked as required but MYOB have modified the UI.
Just for reference I can GET contacts, accounts and taxtypes from MYOB, just getting the Forbidden 403 message back trying to POST an Invoice.
Any help you could provide would be very much appreciated.
If you are getting 403 Forbidden, you need to check the permissions on the account that you are using to make the post call.
See here to read about the permissions of the account
Except from link above
How do I check a user's access permissions
To find out exactly what rights the current user has, and to ensure they have the right permissions for your application to function correctly make a GET request to the {{company_file_uri}}/{{company_file_id}}/CurrentUser endpoint.
Following response tells you what permissions the user has on each url
{
"UserAccess": [
{
"ResourcePath": "https://{{company_file_uri}}/{{company_file_id}}/Banking/BankAccount/",
"Access": [
"GET"
]
},
{
"ResourcePath": "https://{{company_file_uri}}/{{company_file_id}}/Banking/ReceiveMoneyTxn/",
"Access": [
"GET",
"POST",
"PUT",
"DELETE"
]
},
...
]
}
I am getting below error when calling Docusign API from a C# web api. Able to get the access token but when creating the envelope this error is being received.
Is there any issue with clientUserId because it worked without any hiccups in sandbox. What value do I need to pass in it ? From all the sources, I gather it just indicates that this request is an embedded one. If we have to pass a specific userId in this field how to get it when passing it for envelope creation.
Response:
{
"errorCode": "INVALID_USERID",
"message": "Invalid UserId."
}
Below is the request which we are passing
{
"documents": [
{
"documentId": "1",
"fileExtension": "pdf",
"name": "Trial - OL.pdf"
}
],
"emailSubject": "Docusign Digital Signature",
"recipients": {
"signers": [
{
"clientUserId": "1001",
"email": "XXXX",
"name": "XXXX",
"recipientId": "1",
"routingOrder": "1",
"tabs": {
"signHereTabs": [
{
"anchorIgnoreIfNotPresent": "false",
"anchorString": "XXXX",
"anchorUnits": "inches",
"anchorXOffset": "0",
"anchorYOffset": "-0.25"
}
]
}
}
]
},
"status": "sent"
}
There is no error while retreiving access token
The error is not about clientUser but about the userId of the user.
After you finished Go-Live, the account is different, the user is different, and the URLs for the environments are all different when you migrate from the developer sandbox to the production environment.
If you got a token using JWT, remember that one of the things you used was the userId of the impersonated users.
You cannot use the token generator tokens in production.
Production environment doesn't have a single URL like demo.docusign.net. It can be many different URLs and you have to first figure out what it is before making API calls.
I have an API Gateway (C#, net.Core 3.1, Ocelot), everything is working fine but now I'm trying to configure different routes for upstream and downstream, because my Gateway retrieve information during the process and send this information to the final API.
In upstream I don't have a placeholder {userid}, but I want to have it in downstream.
Here is my ocelot.json:
"DownstreamPathTemplate": "/api/User/{userid}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 44301
}
],
"UpstreamPathTemplate": "/api/User/",
"UpstreamHttpMethod": [ "Get" ],
And there is how I'm adding in the middleware the placeholder value:
if (context.DownstreamRequest.OriginalString.Contains("User"))
{
context.DownstreamRequest.AbsolutePath =
context.DownstreamRequest.AbsolutePath + userid; //this variable is valued before
}
So, to be more clear, here is an example:
I get called at http://localhost:44358/api/User/ (mygateway Upstream), from some logics I get the userid that made this call, for example Andrew, and I want to redirect the request to my API http://localhost:44301/api/User/Andrew (mygateway Downstream).
Everything is fine, except at my API the userid is coming as {userid} and not has userid value (Andrew).
You can implement using Claims Transformation feature of Ocelot.
e.g.
"AddQueriesToRequest": {
"LocationId": "Claims[LocationId] > value",
}
I managed to do it with this code in the config:
"DownstreamPathTemplate": "/api/User/{userid}",
"DownstreamScheme": "http",
"ChangeDownstreamPathTemplate": {
"userid": "Claims[userId] > value"
},
"UpstreamPathTemplate": "/api/User/",
Using an ASP.NET Web API 2 OData service I have exposed the the following endpoint:
/entity1('key')/entity2('key')/entity3('key')/entity4('key')
When I execute /entity1('key')/entity2('key')/entity3?$expand=entity4 I receive a JSON response with the correct "#odata.context":
"#odata.context": "https://host:port/$metadata#entity1('key')/entity2('ket')/entity3",
"value": [
{
...,
"entity4#odata.context": "https://host:port/$metadata#entity1('key')/entity2('key')/entity3('key')/entity4",
"entity4": [
{
...
}
]
},
...
When I execute /entity1('key')/entity2('key')/entity3('key')?$expand=entity4 however, I receive an incorrect "#odata.context" with two keys:
"#odata.context": "https://host:port/$metadata#entity1('key')/entity2('ket')/entity3",
"value": [
{
...,
"entity4#odata.context": "https://host:port/$metadata#entity1('key')/entity2('key')/entity3('key')('key')/entity4",
"entity4": [
{
...
}
]
}
This causes an exception when executing with Microsoft's OData Client:
"The context URL 'https://host:port/$metadata#entity1('key')/entity2('key')/entity3('key')('key')/entity4' is invalid."
Well no kidding. What could be causing this?
Update
I've logged an issue here. We'll see what happens.
I've found a workaround:
Using a $filter instead of a key: /entity1('key')/entity2('key')/entity3?$filter=Key eq 'key'&$expand=entity4 provides the correct response:
"#odata.context": "https://host:port/$metadata#entity1('key')/entity2('ket')/entity3",
"value": [
{
...,
"entity4#odata.context": "https://host:port/$metadata#entity1('key')/entity2('key')/entity3('key')/entity4" ,
"entity4": [
{
...
}
]
}